5
\$\begingroup\$

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?
\$\endgroup\$
  • \$\begingroup\$ Did you notice that in your ParamPack<T> initializer list test that the size is incorrect? \$\endgroup\$ – Mike Borkland Nov 10 '18 at 22:50
  • \$\begingroup\$ @MikeBorkland I sort of know what your are asking, but I'm not sure exactly... I'm not quite grasping where you are inferring that the size is incorrect from the initializer list. I've used both constructors and the size being returned for the number of elements so far as been correct for me. \$\endgroup\$ – Francis Cugler Nov 11 '18 at 7:45
  • \$\begingroup\$ @MikeBorkland The code above does not show the initializer_list constructor in use, all the code from main is using the variadic 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\$ – Francis Cugler Nov 11 '18 at 7:47
  • \$\begingroup\$ @MikeBorkland I think I know what you meant now by the size being wrong. I had changed how the size_ variable was being calculated, instead of calling values_.size() I used size_( 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\$ – Francis Cugler Nov 11 '18 at 8:14
3
\$\begingroup\$
  • 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 () as const size_t but the const is useless here.
  • Try to avoid static_cast when you can.
  • To initialize list_, you can use sizeof... () instead of calling size () 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 of std::vector. ( and look at his interface, for a good example of const-correctness.)
\$\endgroup\$
  • \$\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 Nov 11 '18 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 Nov 11 '18 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 Nov 11 '18 at 7:55
  • \$\begingroup\$ I replaced size = values_.size() with size = sizeof...(U) in both constructors. \$\endgroup\$ – Francis Cugler Nov 11 '18 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 Nov 11 '18 at 7:58
2
\$\begingroup\$
#include <vector>
#include <string>
#include <sstream>
#include <iostream>
#include <exception>
#include <type_traits>
#include <initializer_list>
#include <utility>

Do the following:

  1. Split your test code from your library code.
  2. Organize your headers so it's easier to find if a header has been included.
  3. 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:

  1. 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.
  2. 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.

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:

  1. 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),
  2. Default construct to a sane initial state (nullptr),
  3. Only support reference operations (no operator[] meant for arrays/strings, no pointer arithmetic meant for pointers),
  4. Strict-weak ordering guarantee, and
  5. 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.

\$\endgroup\$
  • \$\begingroup\$ Very nice write up with concise yet detailed explanations. I truly appreciate your time and effort. \$\endgroup\$ – Francis Cugler Nov 24 '18 at 5:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.