I have an inheritance class that mimics the behavior of C style array's brace initialization by using a class template that has a variadic constructor but the template itself is not a variadic template. The base class stores the contents from two flavors of constructors either a Variadic Constructor, or a Variadic Constructor that takes an std::initializer_list
as it's parameter. The base class also contains a size that is inferred by its populated member vector, a function to return its size and an overloaded operator operator()()
that will return its internal vector.
The child class inherits this functionality. It only has one member and that is a pointer to <T>
. It has the corresponding matching constructors as its parent class, a set of overloaded subscript-indexing operators and a public method to retrieve it's internal pointer. This class's internal pointer is set to point to its parent's internal vector's data.
As far as I can tell this appears to be bug free to the best of my knowledge and it is producing expected values. Here is what I have so far.
#include <vector>
#include <string>
#include <sstream>
#include <iostream>
#include <exception>
#include <type_traits>
#include <initializer_list>
#include <utility>
template<typename T>
class ParamPack {
protected:
std::vector<T> values_;
size_t size_;
public:
template<typename... U>
ParamPack( U... u ) : values_{ static_cast<T>(u)... } {
size_ = values_.size();
}
template<typename ... U>
ParamPack( std::initializer_list<std::is_same<T, U...>( U...)> il ) : values_( il ) {
size_ = values_.size();
}
std::vector<T> operator()() {
return values_;
}
const size_t size() const { return size_; }
};
template<typename T>
class Array : public ParamPack<T> {
private:
T* items_;
public:
template<typename... U>
Array( U... u ) : ParamPack<T>::ParamPack( u... ) {
items_ = this->values_.data();
}
template<typename... U>
Array( std::initializer_list<U...> il ) : ParamPack<T>::ParamPack( il ) {
items_ = this->values_.data();
}
T& operator[]( int idx ) {
return items_[idx];
}
T operator[]( int idx ) const {
return items_[idx];
}
T* data() const { return items_; }
};
int main() {
try {
// Parameter Pack Examples:
// Variadic Constructor { ... }
std::cout << "ParamPack<T> Examples:\n";
std::cout << "Using ParamPack<T>'s Variadic Constructor\n";
ParamPack<int> pp1( 1, 2, 3, 4 );
std::cout << "Size: " << pp1.size() << " | Elements: ";
for( auto& v : pp1() ) {
std::cout << v << " ";
}
std::cout << '\n';
//std::initializer_list<int> il{ 1,2,3,4 };
std::cout << "Using ParamPack<T>'s Initializer List\n";
ParamPack<int> pp2( { 5, 6, 7, 8 } );
std::cout << "Size: " << pp2.size() << " | Elements: ";
for( auto& v : pp2() ) {
std::cout << v << " ";
}
std::cout << "\n\n";
// Array Examples:
// Using Variadic Constructor
std::cout << "Array<T> Examples:\n";
std::cout << "Using Array<T>'s Variadic Constructor\n";
Array<int> testA( 9, 8, 7, 6 );
for( size_t i = 0; i < testA.size(); i++ ) {
std::cout << testA[i] << " ";
}
std::cout << '\n';
Array<std::string> testB( "Hello", " World" );
for( size_t i = 0; i < testB.size(); i++ ) {
std::cout << testB[i] << " ";
}
std::cout << "\n\n";
// Using Constructor w/ Initializer List
std::cout << "Using Array<T>'s Constructor with Initializer List\n";
Array<int> testC( { 105, 210, 420 } );
for( size_t i = 0; i < testC.size(); i++ ) {
std::cout << testC[i] << " ";
}
std::cout << "\n\n";
// Using Initializer List with =
std::cout << "Using Array<T>'s Initializer List with =\n";
Array<int> a = { 1, 2, 3, 4 };
for( size_t i = 0; i < a.size(); i++ ) {
std::cout << a[i] << " ";
}
std::cout << '\n';
Array<char> b = { 'a', 'b', 'c', 'd' };
for ( size_t i = 0; i < b.size(); i++ ) {
std::cout << b[i] << " ";
}
std::cout << '\n';
Array<double> c = { 1.2, 3.4, 4.5, 6.7 };
for( size_t i = 0; i < c.size(); i++ ) {
std::cout << c[i] << " ";
}
std::cout << "\n\n";
// Using Initializer List directly
std::cout << "Using Array<T>'s Initalizer List directly\n";
Array<uint32_t> a1{ 3, 6, 9, 12 };
for( size_t i = 0; i < a1.size(); i++ ) {
std::cout << a1[i] << " ";
}
std::cout << "\n\n";
// Using user defined data type
struct Point {
int x_, y_;
Point( int x, int y ) : x_( x ), y_( y ) {}
};
Point p1( 1, 2 ), p2( 3, 4 ), p3( 5, 6 );
// Variadic Constructor
std::cout << "Using Array<T>'s Variadic Consturctor with user data type\n";
Array<Point> d1( p1, p2, p3 );
for( size_t i = 0; i < d1.size(); i++ ) {
std::cout << "(" << d1[i].x_ << "," << d1[i].y_ << ") ";
}
std::cout << '\n';
// Initializer List Construtor (reversed order)
std::cout << "Using Array<T>'s Initializer List Constructor with user data type\n";
Array<Point> d2( { p3, p2, p1 } );
for( size_t i = 0; i < d2.size(); i++ ) {
std::cout << "(" << d2[i].x_ << "," << d2[i].y_ << ") ";
}
std::cout << '\n';
// Initializer List Version = {...} p2 first
std::cout << "Using Array<T>'s = Initializer List with user data type\n";
Array<Point> d3 = { p2, p1, p3 };
for( size_t i = 0; i < d3.size(); i++ ) {
std::cout << "(" << d3[i].x_ << "," << d3[i].y_ << ") ";
}
std::cout << '\n';
// Initializer List Directly p2 first p1 & p3 swapped
std::cout << "Using Array<T>'s Initializer List directly with user data type\n";
Array<Point> d4{ p2, p3, p1 };
for( size_t i = 0; i < d4.size(); i++ ) {
std::cout << "(" << d4[i].x_ << "," << d4[i].y_ << ") ";
}
std::cout << "\n\n";
// Need a local copy of the vector instead?
std::cout << "Using Array<T>'s base class's operator()() to retrieve vector\n";
std::vector<Point> points = d4(); // using operator()()
for( auto& p : points ) {
std::cout << "(" << p.x_ << "," << p.y_ << ") ";
}
std::cout << '\n';
// Need a local copy of the pointer instead?
std::cout << "Using Array<T>'s data() to get the contents of its internal pointer\n";
Point* pPoint = nullptr;
pPoint = d4.data();
for( size_t i = 0; i < d4.size(); i++ ) {
std::cout << "(" << pPoint[i].x_ << "," << pPoint[i].y_ << ") ";
}
std::cout << '\n';
// Will Not Compile: Must Be Instantiated By Type Explicitly
// Array e( 1, 2, 3, 4 );
// Array e( { 1,2,3,4 } );
// Array f{ 1, 2, 3, 4 };
// Array g = { 1,2,3,4 };
} catch( const std::runtime_error& e ) {
std::cerr << e.what() << '\n';
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
-Output-
ParamPack<T> Examples: Using ParamPack<T>'s Variadic Constructor Size: 4 | Elements: 1 2 3 4 Using ParamPack<T>'s Initializer List Size: 5 | Elements: 5 6 7 8 Array<T> Examples: Using Array<T>'s Variadic Constructor 9 8 7 6 Hello World Using Array<T>'s Constructor with Initializer List 105 210 420 Using Array<T>'s Initializer List with = 1 2 3 4 a b c d 1.2 3.4 5.6 7.8 Using Array<T>'s Initializer List directly 3 6 9 12 Using Array<T>'s Variadic Constructor with user data type (1,2) (3,4) (5,6) Using Array<T>'s Initializer List Constructor with user data type (5,6) (3,4) (1,2) Using Array<T>'s = Initializer List with user data type (3,4) (1,2) (5,6) Using Array<T>'s Initializer List directly with user data type (3,4) (5,6) (1,2) Using Array<T>'s base class's operator()() to retrieve vector (3,4) (5,6) (1,2) Using Array<T>'s data() to get the contents of its internal pointer (3,4) (5,6) (1,2)
There are few things I'd like to know about this code: The main concerns are more towards the classes themselves than the actual code use within the main function. However, suggestions towards the use of the class are accepted as well.
- Does this follow modern best practices? Note: I know I can encapsulate it in an
namespace
but for simplicity I just posted the classes themselves- If not what changes or fixes need to be made?
- Is it considered readable enough?
- Is the code efficient?
- What can be done to improve its efficiency?
- Are there any unforeseen bugs or gotchas that I overlooked?
- Is this considered portable, reusable, and generic?
- Will the derived class invoke the appropriate constructor or will it just fall back to one of them?
2 Answers 2
- I think you should make your interface
constexpr
- Did you expect negative index? If not, try to use
std::size_t
instead of int. - Does returning
values_
by value is a design choice? - You specify the return type of
size ()
asconst size_t
but the const is useless here. - Try to avoid
static_cast
when you can. - To initialize
list_
, you can usesizeof... ()
instead of callingsize ()
method, and adding it at the end of the member initializer lists. - Since your parameter pack is know at compile time, have you tried to use
std::array
instead ofstd::vector
. ( and look at his interface, for a good example of const-correctness.)
-
\$\begingroup\$ In the ParamPack I changed the function to get the size to just return size_t instead of const size_t. I also changed the operator()() to return std::vector<T>& instead. In Array<T> I changed the operators to take a size_t instead of an int. These were simple easy corrections. \$\endgroup\$Francis Cugler– Francis Cugler2018年11月11日 07:52:15 +00:00Commented Nov 11, 2018 at 7:52
-
\$\begingroup\$ As for the static_cast in ParamPack<T>'s Variadic Constructor, I've tried removing it and MSVC yells at me, but as soon as I use it all compiler warnings - errors go away. \$\endgroup\$Francis Cugler– Francis Cugler2018年11月11日 07:53:57 +00:00Commented Nov 11, 2018 at 7:53
-
\$\begingroup\$ As for using std::array instead of std::vector that should not be that hard to modify and I can work on that at a later time. \$\endgroup\$Francis Cugler– Francis Cugler2018年11月11日 07:55:28 +00:00Commented Nov 11, 2018 at 7:55
-
\$\begingroup\$ I replaced
size = values_.size()
withsize = sizeof...(U)
in both constructors. \$\endgroup\$Francis Cugler– Francis Cugler2018年11月11日 07:58:14 +00:00Commented Nov 11, 2018 at 7:58 -
\$\begingroup\$ The first point you made by making the interface
constexpr
that might take a little more work as I'm not very family with all of the proper syntax, but I'll continue to work on that at a later time. \$\endgroup\$Francis Cugler– Francis Cugler2018年11月11日 07:58:59 +00:00Commented Nov 11, 2018 at 7:58
#include <vector>
#include <string>
#include <sstream>
#include <iostream>
#include <exception>
#include <type_traits>
#include <initializer_list>
#include <utility>
Do the following:
- Split your test code from your library code.
- Organize your headers so it's easier to find if a header has been included.
- Remove headers that are not used. (
<sstream>
,<exception>
)
template<typename T>
class ParamPack {
protected:
std::vector<T> values_;
size_t size_;
Using std::vector
introduces two problems:
- Allocation
- Data is on the heap instead of the stack.
- You discarded the size, which is known at compile time, just to be able to derive the size later.
- Object Size
- You are storing
- the data on the heap,
- a pointer in
Array
to the data, - three pointers in
std::vector<>
to the data, and - the size of the
std::vector<>
.
- A
std::array<>
would simply store the data on the stack.
- You are storing
public:
template<typename... U>
ParamPack( U... u ) : values_{ static_cast<T>(u)... } {
size_ = values_.size();
}
template<typename ... U>
ParamPack( std::initializer_list<std::is_same<T, U...>( U...)> il ) : values_( il ) {
size_ = values_.size();
}
const size_t size() const { return size_; }
The constructor that takes the initializer list is never taken as substitution failure simply causes it to always be removed from the overload set. In your test code, the implicitly constructed lists are not std::initializer_list<int>
types and won't take that overload.
Your arguments in the variadic are passed by value. You end up copying u
in Array::Array(...)
, ParamPack::ParamPack(...)
, and std::vector<int>::std::vector<int>(std::initializer_list<int>)
. Read up on perfect forwarding.
Don't store size_
. Just call values_.size()
whenever you need the size.
Qualify size()
with the [[nodiscard]]
attribute.
std::vector<T> operator()() {
return values_;
}
Did you intend to return by value?
T operator[]( int idx ) const {
return items_[idx];
}
Did you intend to return by value?
You have an implicit conversion on access std::vector
that changes the signedness of the index. std::vector<>::operator[]
expects the index type to be std::vector::size_type
, which is often std::size_t
.
int main() { ... }
It is great everything prints out, but are the observed results the expected results? Use a testing framework.
Does this follow modern best practices? Note: I know I can encapsulate it in an namespace but for simplicity I just posted the classes themselves
Namespace wrapping is definitely a starter. You should minimize the number of symbols you introduce into the global namespace.
Prefer the qualified standard types (::std::size_t
) instead of the unqualified types (::size_t
). The C++ Standard makes no guarantee that the unqualified symbols exist. See C++17 draft n4659 § 15.5.4.3.4/1.
Use std::array<>
when the container has a fixed size known at compile-time.
If you want your container to be used as if it were a C++ container, you must fulfill the standard general container requirements.
Don't use a raw pointer (T*
). Use alternative solutions. std::observer_ptr
in C++20 doesn't require any special feature that would prevent it from being implemented in earlier C++ revisions. The reason for the existence of the world's dumbest smart pointer is to:
- Self-document that the pointer is a non-owning reference to an object (not an owning-reference, built-in array, built-in string or iterator),
- Default construct to a sane initial state (
nullptr
), - Only support reference operations (no
operator[]
meant for arrays/strings, no pointer arithmetic meant for pointers), - Strict-weak ordering guarantee, and
- Discourages using unsafe
void*
.
Are there any unforeseen bugs or gotchas that I overlooked?
You should be aware of class template argument deduction guides introduced in C++17. The compiler can deduce both the size and the type from the arguments provided. Here is the deduction guide for std::array
.
std::array a1{1, 2, 42}; // deduced as std::array<int, 3>
std::array a2{1, 2, 42u}; // ill-formed as types are not the same.
You can also write your own guide to derive the type while allowing the user to provide a type.
-
\$\begingroup\$ Very nice write up with concise yet detailed explanations. I truly appreciate your time and effort. \$\endgroup\$Francis Cugler– Francis Cugler2018年11月24日 05:23:04 +00:00Commented Nov 24, 2018 at 5:23
Explore related questions
See similar questions with these tags.
ParamPack<T>
initializer list test that the size is incorrect? \$\endgroup\$initializer_list
constructor in use, all the code from main is using thevariadic
constructor. To simply use the initializer_list version, just create an initializer_list beforehand and pass that into the constructor. I've tried many examples and nothing has crashed and I'm getting a code of(0)
when the program exits. \$\endgroup\$size_
variable was being calculated, instead of callingvalues_.size()
I usedsize_( sizeof...(U) )
in the constructors initializer list. From here I noticed that when I used the constructor that takes an initializer list it was reporting size of 1 and not the size of the elements. So for the 2nd constructor I now calculate it like:size_ = il.size();
within the constructor. I might move this to the constructors initializer list. \$\endgroup\$