Search code examples
c++templatesinheritanceconstructorsfinae

C++ SFINAE constructor accepting derived class too


Primer: C++ base class constructor taking derived class as argument (?)

I have a Vector and a Vector2D class. The former should contain a constructor that allows element-wise type casting. It should be allowed for the derived class too. For some cases, it already works, (see examples below) but i think some SFINAE magic is what's missing.

Vector

#include <array>
#include <type_traits>

namespace mu {
template<std::size_t N, typename T>
class Vector {
public:
    // ...

    template <typename... TArgs,
        std::enable_if_t<sizeof...(TArgs) == N ||
                         (!std::is_base_of_v<Vector, TArgs> && ...), int> = 0>
    Vector(TArgs... args) : data({args...}) {}

    // this should always be called for type casting:
    template <std::size_t Nn, typename Tt>
    Vector(const Vector<Nn, Tt> &other) {
        static_assert(N == Nn, "Vector size mismatch");
        for (std::size_t i = 0; i < N; i++) {
            data[i] = static_cast<T>(other[i]);
        }
    }

    Vector(const Vector &other) = default; // copy constructor

protected:
    std::array<T, N> data;
};
}

Vector2D

namespace mu {
template<typename T>
class Vector2D : public Vector<2,T> {

  public:

  using Vector<2, T>::Vector; // inherit base class constructors

  Vector2D(const Vector<2, T>& other) : Vector<2, T>(other) {}

  // Vector2D specific functions, e.g. rotation
  //...

};
}

Examples (they should all compile)

// Example 1 (compiles)
mu::Vector<2, int> a{1, 2};
mu::Vector<2, float> b{a};

// Example 2 (compiles)
mu::Vector<2, int> c{1, 2};
mu::Vector2D<float> d{c};

// Example 3 (doesn't compile)
mu::Vector2D<int> e{1, 2};
mu::Vector<2, float> f{e};

// Example 4 (doesn't compile)
mu::Vector2D<int> g{1, 2};
mu::Vector2D<float> h{g};

Error (examples 3 & 4)

.../vector.h:63:27: error: no matching constructor for initialization of 'std::array<float, 2UL>'
Vector(TArgs... args) : data_({args...}) {}

The code tries to call the wrong constructor instead of the one that contains the type casting. I already tried to add another constraint to the enable_if term but so far with no good result.


Solution

  • @Jarod42 pointed me in the right direction

    What i need is && instead of || for the two arguments. However, std::is_base_of is not really enforcing what i want. I replaced it with std::is_same to check if all the arguments supplied by ...TArgs are actually of the same type as the one that the Vector already holds, T.

    Now, this constructor can only be called by giving it the exact N number of arguments that are of type T

    template <typename... TArgs,
           std::enable_if_t<sizeof...(TArgs) == N && 
                            (std::is_same_v<T, TArgs> && ...), int> = 0>
    Vector(TArgs... args) : data_({args...}) {}