Search code examples
c++templatesc++20variadic-templatesclass-template

How to declare the constructor with exactly nRow*nCol arguments of type T?


I am a newbie in C++ and just learned how to use templates. I want to write a Matrix class with the following template arguments:

template < int nRow ,int nCol ,typename T = double >
class ZDMatrix  {
public:
    ZDMatrix();
    ZDMatrix( T arg0 );
    ZDMatrix( T arg0 ,T arg1 );
    // ... How to declare the constructor with exactly nRow*nCol arguments of type T ??
    // ZDMatrix( T arg0 ,T arg1 ,... ) {
    //     ... number or argN should be nRow*nCol
    // }
};

I want to be able to use the matrix like this:

ZDMatrix<2,3>   matX( 1 ,1.1 ,1.2 ,2,2.1,2.2 );  // auto convert int 1 to double 1.0

As an alternative, I already try using std::initializer_list:

ZDMatrix<nRow,nCol,double>::ZDMatrix( std::initializer_list<T> list )  {
    assert( list.size() == nRow*nCol );
    // ...
}

But the check for the number of arguments is at run time not static_assert().


Solution

  • How do you declare the constructor with exactly nRow*nCol arguments of type T ??

    One way is by using sizeof..., the number of elements in a parameter pack can be checked, assuming you use the constructor with variadic template arguments. Applying SFINAE, it would look like:

    #include <type_traits> // std::conjunction (since c++17), std::enable_if, std::is_same
    
    template <std::size_t nRow, std::size_t nCol, typename T = double>
    class ZDMatrix
    {
       std::array<T, nRow* nCol> mData{};
    
    public:
       template <typename... Ts, 
          std::enable_if_t<sizeof...(Ts) == nRow * nCol &&  // No. of args must be equal tonRow * nCol
          std::conjunction_v<std::is_same<T, std::remove_cvref_t<Ts>>...> // or std::is_convertible, std::decay_t<Ts>
          >* = nullptr>
          ZDMatrix(Ts&&... args)
          : mData{ std::forward<Ts>(args)... }
       {}
    };
    

    See live demo


    Alternatively, you could static_assert the above two SFINAE conditions inside the constructor body. Or in C++20 you would do requires constraints for the same conditions as shown below:

    template <std::size_t nRow, std::size_t nCol, typename T = double>
    class ZDMatrix
    {
       std::array<T, nRow* nCol> mData{};
    
    public:
       template <typename... Ts>
          requires (sizeof...(Ts) == nRow * nCol) && (std::same_as<T, std::remove_cvref_t<Ts>> && ...)
       ZDMatrix(Ts&&... args)
          : mData{ std::forward<Ts>(args)... }
       {}
    };
    

    See live demo


    As a side note, the compiler will always tell the type mismatch while using the templated constructor. I would choose a template solution unless there is a special requirement to have std::initializer_list<T> and assert macro.