Search code examples
c++templatesc++11sfinaeenable-if

Differentiate between 1D and 2D container in template class constructor (SFINAE)


So, I have a class, which has an array of arrays as a private member. I wish to have two constructors for each case (1D or 2D). But of course their declaration happens to be the same, so template deduction can't do its job without me doing something about it. Here's the code:

Edit: I also need it to work with STL containers like vector or C++ array. That is why I am overcomplicating and not going with the "arrays" fix.

#include <iostream>
#include <array>

template<class T, std::size_t rows_t, std::size_t cols_t>
class test
{
private:
    std::array<std::array<T, cols_t>, rows_t> _data;
public:    
    auto begin() { return this->_data.begin(); }
    auto end() { return this->_data.end(); }


    //CONSTRUCTOR
    template<class type_t>
    test(const type_t &arr)
    {
        std::size_t j = 0;
        for (const auto &num : arr)
            this->_data[0][j++] = num;
    }

    template<class type_t>
    test(const type_t &arr)
    {
        std::size_t i = 0;
        for (const auto &el : arr)
        {
            std::size_t j = 0;
            for (const auto &num : el)
                this->_data[i][j++] = num;
            ++i;
        }
    }
};

int main()
{
    double arr[3] = { 1, 2, 3 };
    double arr2[2][2] = { {1, 2}, {3, 4} };

    test<double, 1, 3> obj = arr; 
    test<double, 2, 2> obj2 = arr2;

    for (const auto &i : obj2)
    {
        for (const auto &j : i)
            std::cout << j << " ";
        std::cout << std::endl;
    }

    std::cin.get();
}

Note: I've been reading about enable_if, but I don't quite understand how it works. Can it be done with that?


Solution

  • First, you have to "teach" the compiler what's 2D and what's not. Hence, you have to define something like the following type trait:

    template<typename T>
    struct is2D : public std::false_type {};
    template<typename T, std::size_t N, std::size_t M>
    struct is2D<std::array<std::array<T, M>, N>> : std::true_type {};
    template<typename T>
    struct is2D<std::vector<std::vector<T>>> : std::true_type {};
    template<typename T, std::size_t N, std::size_t M>
    struct is2D<T[N][M]> : std::true_type {};
    

    Then you could set up your class definition in the following way:

    template<class T, std::size_t rows_t, std::size_t cols_t>
    class test{
      std::array<std::array<T, cols_t>, rows_t> _data;
    
      template<class type_t>
      std::enable_if_t<!is2D<type_t>::value, void>
      test_init(type_t const &arr) {
        std::size_t j = 0;
        for (const auto &num : arr) _data[0][j++] = num;
      }
    
      template<class type_t>
      std::enable_if_t<is2D<type_t>::value, void>
      test_init(type_t const &arr) {
        std::size_t i = 0;
        for(const auto &el : arr) {
          std::size_t j = 0;
          for (const auto &num : el) _data[i][j++] = num;
          ++i;
        }
      }
    
    public:
    
      auto &operator[](const std::size_t &i) { return this->_data[i]; }
      auto begin() { return this->_data.begin(); }
      auto end() { return this->_data.end(); }
    
      //CONSTRUCTOR
      template<class type_t> test(type_t const &arr) { test_init(arr); }
    };
    

    LIVE DEMO