根据模板参数有条件地定义模板类的构造函数

时间:2016-02-15 01:59:08

标签: c++ templates

我有以下课程,用作2D / 3D /等空间中的通用"点/矢量":

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);
}

这个类上有更多的运算符等,但为了简洁,我省略了大部分运算符。我的问题是,如何定义此类的构造函数?

当维度为2时,我想要一个带有2个参数的构造函数:

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

当维度为3时,我想要一个带有3个参数的构造函数:

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

按照设计,这个类支持任意维度,因此为每个维度创建一个特殊化并不是一个有吸引力的方法,也不是为每个支持的维度定义一个支持SFINAE的构造函数。如何为Vector<N>编写一个构造函数,它接受N个参数并将它们作为初始化列表传递给数据数组?

4 个答案:

答案 0 :(得分:2)

有一些标签类型。首先是private_tag。第二个count_tag<N>

拥有一个需要T0Ts的变量ctor。它sfinae - 防止T0成为private_tag,然后将其参数转发给ctor Vector(private_tag{}. count_tag<sizeof...(Ts)+1>{}, t0, ts...)

您现在可以创建一个私有ctor,它首先接受私有标记,然后是正确的count标记,并构建您的数组。

错误会提到无法转换计数标签。

通过使用基类进行存储,并使用count标记构建它,可以使私有标记技巧错误消息更好。

第二种方法是使用静态断言。为了在你击中阵列的ctor之前击中它,你会想要使用一些技巧,比如一个空基类,它需要Ts&&并且在它的体内有静态断言(但是没有别的)。

如果您不介意接受大小为3的向量的2个元素,请使用std::array<float,3> ctor。

为向量3合成std::tuple<float,float,float>类型,然后拥有该ctor。使用c ++ 17,你得到MyVec({3.14f, 0f, 2f}),在C ++ 11中你需要MyVec(std::make_tuple(3.14, 0, 2)),但它运行良好。将工作变成元组。您还可以使用必需的隐式ctorm在C ++ 11中编写自定义类型,为您提供良好的C ++ 17语法。

像这样:

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

有用的样板。然后:

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

重复获取一个计数和一个类型的模板,并重复将它们传递给模板的类型计数次数。

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

这给了我们ntuple,它接受​​一个类型和一个计数,并返回一个元组:

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

有很多方法可以写重复。这是一个简单的方法:

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>
{};

并完成了。如果缺乏索引序列支持,那么SO上有许多高质量的实现。寻找具有日志递归深度的文件。

答案 1 :(得分:2)

这是你在找什么?

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");
}

答案 2 :(得分:2)

Clang和MSVC都是,或者很快将使用内在函数实现ndarray,GCC / libstdc ++现在有一个基于库的O(log N)实现,所以我们不要重新发明“如何”如何得到一个特定尺寸的包装“。构建索引序列,并使用别名模板通过包扩展生成所需的类型列表。

std::make_integer_sequence

答案 3 :(得分:1)

我认为这个解决方案使设计更加丑陋,但它提供了你想要的构造函数。

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派生自VectorCtor类并使用它的构造函数。 Vector<2>VectorCtor<float, float>作为父级,并继承VectorCtor(float, float)构造函数。 MakeCtor类只是通过模板递归将Vector的维度转换为正确数量的float参数。

我更喜欢具有设置签名的函数而不是在传递错误参数时失败的可变参数。这样,至少在Visual Studio中,我可以看到签名就像我打字一样,当我的参数数量错误时,IntelliSense会突出显示。

更新:所以,兔子洞下来一点点。正如@Yakk告诉我的那样,模板递归会减慢构建速度并在大小太大时崩溃。对于我的编译器,默认上限约为500.一个大于那个不会编译的数组。

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

我不确定如何为这样的模板实现二进制递归,所以我估计直到某些东西有效。这绝对可以处理更大的尺寸。

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只需稍作修改即可:

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

Node结构保存大小,Split递归地将每个Node拆分为两个Node的一半大小。这一直持续到大小为1,它变为类型(此处为float),然后全部放入VectorCtor。我的猜测是现在的限制大约是2 ^ 500,但是如果我远远超过10,000-ish,则编译器会耗尽内存。这个大小的构建非常慢,但是合理的大小,比如1024似乎很好。