mtosch
mtosch

Reputation: 461

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.

Upvotes: 0

Views: 131

Answers (2)

mtosch
mtosch

Reputation: 461

@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...}) {}

Upvotes: 0

Jarod42
Jarod42

Reputation: 217165

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

is wrong, you probably want && Demo

but std::is_convertible<TArgs, T> or std::is_constructible<T, TArgs> seems more appropriate.

variadic constructor without "tag" is dangerous BTW, as it can catch easily copy constructor.

Upvotes: 3

Related Questions