Elliott
Elliott

Reputation: 1376

Conditionally defining the constructor of a template class, based on the template argument

I have the follow class, used as a generic "Point/vector in 2D/3D/etc space":

template<size_t dimension>
class Vector
{
private:
    std:array<float, dimension> data;
public:
    //common vector operations etc
    inline float magnitude() const;
    inline Vector<dimension> normalized() const;
    inline static float dotProduct(const Vector<dimension>& left, const Vector<dimension>& right);

    //vector arithmetic operators etc
    inline Vector<dimension> &operator+=(const Vector<dimension> &other);
    inline Vector<dimension> &operator*=(float s);
}

There are way more operators and such on this class, but I omitted most of them for brevity. My question is, how can I define the constructor for this class?

When dimension is 2, I want a constructor that takes 2 arguments:

Vector<2>::Vector(float x, float y) : data({x,y}) {}

When dimension is 3, I want a constructor that takes 3 arguments:

Vector<3>::Vector(float x, float y, float z) : data({x,y,z}) {}

By design, this class supports arbitrary dimensions, so creating a specialization for each dimension isn't an attractive approach, neither is defining a SFINAE-enabled constructor for each supported dimension. How can I write a single constructor for Vector<N> that takes in N arguments and passes them as an initializer list to the data array?

Upvotes: 4

Views: 1258

Answers (4)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275220

Have a few tag types. First, a private_tag. Second a count_tag<N>.

Have a variaridic ctor that takes T0 and Ts. It sfinae-prevents T0 from being a private_tag, then forwards its arguments to the ctor Vector(private_tag{}. count_tag<sizeof...(Ts)+1>{}, t0, ts...).

You can now make a private ctor that first takes private tag, then then the proper count tag, and constructs your array.

Errors will mention being unable to convert the count tags.

The private tag trick error messages can be made better by using a base class for storage, and still constructing it with the count tag.


A second approach is to use a static assert. In order to hit it before you hit the ctor for the array, you'll want to use some tricks, like an empty base class that takes Ts&& and has the static assert in its body (but does nothing else).


If you do not mind accepting 2 elements for a vector of size 3, have a std::array<float,3> ctor.


Synthesize a std::tuple<float,float,float> type for a vector 3, then have that ctor. With c++17 you get MyVec({3.14f, 0f, 2f}),mand in C++11 you need to MyVec(std::make_tuple(3.14, 0, 2)), but it otherwise works well. Punt the job to tuple. You can also write a custom type in C++11 with the requisite implicit ctormto give you the nice C++17 syntax.

Like this:

template<class T>struct tag{using type=T;};
template<class Tag>using type_t=typename Tag::type;

Useful boilerplate. Then:

template<template<class...>class Z, size_t N, class T>
struct repeat;

Repeat takes a template a count and a type, and repeats the type count times passing them to the template.

template<template<class...>class Z, size_t N, class T>
using repeat_t=type_t<repeat<Z,N,T>>;

Which gives us ntuple, which takes a type and a count, and returns a tuple:

template<size_t N, class T>
using ntuple=repeat_t<std::tuple, N, T>;

There are many ways to write repeat. Here is an easy way:

namespace details{
  template<template<class...>class Z, class Ns, class T>
  struct repeat;
  template<std::size_t, class T>using the_T=T;
  template<template<class...>class Z, std::size_t...Ns, class T>
  struct repeat<Z,std::index_sequence<Ns...>,T>:
    tag<Z< the_T<Ns, T>... >>
  {};
}
template<template<class...>class Z, size_t N, class T>
struct repeat:
  details::repeat<Z, std::make_index_sequence<N>, T>
{};

and done. If you lack index sequence support, there are many high quality implementations on SO. Look for one with log recursive depth.

Upvotes: 2

T.C.
T.C.

Reputation: 137301

Both Clang and MSVC are, or will soon be, implementing std::make_integer_sequence with an intrinsic, and GCC/libstdc++ now has a O(log N) library-based implementation, so let's not reinvent the wheel of "how do I get a pack of a particular size". Build an index sequence, and generate the desired list of types by pack expansion with an alias template.

template<size_t dimension,
         class IndexSeq = std::make_index_sequence<dimension>>
class VectorStorage;

template<size_t dimension, size_t... Is>
class VectorStorage<dimension, std::index_sequence<Is...>> {
    template<size_t> using ElementType = float;
protected:
    std:array<float, dimension> data;
public:
    VectorStorage() = default;
    VectorStorage(ElementType<Is>... args) : data{args...} {}
 };

template<size_t dimension>
class Vector : VectorStorage<dimension>
{
public:
    using VectorStorage<dimension>::VectorStorage;
    // other stuff

};

Upvotes: 2

Weak to Enuma Elish
Weak to Enuma Elish

Reputation: 4637

I think this solution makes the design uglier, but it provides the constructor you want.

template <typename... Ts_>
class VectorCtor //Hold the array and provide the ctor for Vector
{
protected:
    std::array<float, sizeof...(Ts_)> _data;
public: //I can't find a way to make this protected otherwise I would
    VectorCtor() = default;
    VectorCtor(const VectorCtor&) = default;
    VectorCtor(Ts_... args) : _data{ args... } {}
};

template <std::size_t X_, typename... Ts_>
struct MakeCtor //Recursively adds floats to Ts_
{
    typedef typename MakeCtor<X_ - 1, Ts_..., float>::type type;
};
template <typename... Ts_>
struct MakeCtor<0, Ts_...> //Base case
{
    typedef VectorCtor<Ts_...> type;
};

template <std::size_t N_>
class Vector : public MakeCtor<N_>::type //Gets parent type for ctor
{
    typedef typename MakeCtor<N_>::type Parent;
public:
    using Parent::Parent; //Use parent class constructors
    //... the rest of it
};

Vector derives from a VectorCtor class and uses it's constructors. A Vector<2> will have VectorCtor<float, float> as a parent, and inherit the VectorCtor(float, float) constructor. The MakeCtor class just converts the dimensions of the Vector into the correct number of float arguments through template recursion.

I prefer functions with a set signature over variadic ones that fail when the wrong arguments are passed. This way, in Visual Studio at least, I can see the signature as I'm typing, and IntelliSense will highlight when I have the wrong number of arguments.

UPDATE: So, down the rabbit hole a little. As @Yakk has informed me, the template recursion will slow down the build and crash when the size is too large. For my compiler, the default upper limit is about 500. An array larger than that won't compile.

fatal error C1202: recursive type or function dependency context too complex

I wasn't sure how to implement binary recursion for a template like this, so I guesstimated until something worked. This can definitely handle larger sizes, at least.

template <typename T_, std::size_t N_>
struct Node{}; //Hold type and recursion depth data

template <typename... Ts_>
struct Wrap //Hold list of types
{
    typedef Wrap<Ts_...> type;
};

template <typename... W1_, typename... W2_, typename... Ts_>
struct Wrap<Wrap<W1_...>, Wrap<W2_...>, Ts_...> //Two Wraps combine to a single Wrap
{
    typedef Wrap<W1_..., W2_..., Ts_...> type;
};

template <typename, typename>
struct Join;

template <typename>
struct Split;

template <typename T_, std::size_t N_>
struct Split<Node<T_, N_>> //Make two Nodes with size N_ / 2
{
    typedef typename Wrap<typename Split<Node<T_, N_ / 2>>::type, typename Split<Node<T_, N_ - N_ / 2>>::type>::type type;
};
template <typename T_>
struct Split<Node<T_, 1>> //Base case
{
    typedef Wrap<T_> type;
};

template <typename>
struct Get;

template <typename... Ts_>
struct Get<Wrap<Ts_...>> //Retrieve
{
    typedef VectorCtor<Ts_...> type;
};

template <typename T_, std::size_t N_>
struct Expand //Interface to start recursion
{
    static_assert(N_ > 0, "Size cannot be zero!");
    typedef typename Get<typename Split<Node<T_, N_>>::type>::type type;
};

template <typename T_, std::size_t N_>
using BaseType = typename Expand<T_, N_>::type; //Less typing

Vector just needs a minor change:

template <std::size_t N_>
class Vector : public BaseType<float, N_> //Change the base class, and typedef too

The Node struct holds the size, and Split recursively splits each Node into two Nodes of half size. This continues until the size is 1, where it becomes the type (float here) and then are all put into the VectorCtor. My guess is that the limit would now be around 2^500, but if I go much higher than 10,000-ish the compiler runs out of memory. Builds of that size are VERY slow, but a reasonable size of, say, 1024 seems fine.

Upvotes: 1

Jts
Jts

Reputation: 3527

Is this what you are looking for?

template <typename ...Args,
    typename std::enable_if_t<dimension == sizeof...(Args), int> = 0>
    Vector(Args&& ...args) : data { std::forward<Args>(args)... }
{ }

template <typename ...Args,
    typename std::enable_if_t<dimension != sizeof...(Args), int> = 0>
    Vector(Args&& ...args)
{
    static_assert(sizeof...(Args) == dimension, "Dimension doesn't match parameter count");
}

Upvotes: 2

Related Questions