基于模板参数有条件地定义模板类的构造函数



我有以下类,用作通用的"2D/3D/etc空间中的点/向量":

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 个参数并将它们作为初始值设定项列表传递给数据数组?

这是你要找的吗?

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

Clang和MSVC都正在或即将实现具有内在函数的std::make_integer_sequence,而GCC/libstdc++现在具有基于O(log N(库的实现,因此我们不要重新发明"如何获得特定大小的包"的轮子。构建索引序列,并使用别名模板通过包扩展生成所需的类型列表。

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

有几种标签类型。 首先,private_tag. 第二个count_tag<N> .

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

您现在可以创建一个私有 ctor,它首先采用私有标签,然后采用正确的计数标签,并构造您的数组。

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

通过使用基类进行存储,并且仍然使用 count 标记构造它,可以更好地制作私有标记技巧错误消息。


第二种方法是使用静态断言。 为了在命中数组的 ctor 之前命中它,您需要使用一些技巧,例如一个空基类,它接受Ts&&并在其主体中具有静态断言(但不执行任何其他操作(。


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


为向量 3 合成一个std::tuple<float,float,float>类型,然后拥有该 ctor。 使用 c++17,您需要MyVec(std::make_tuple(3.14, 0, 2)) C++11 中得到MyVec({3.14f, 0f, 2f}),但除此之外它运行良好。 将作业平移到元组。 您还可以使用必要的隐式 ctormm 在 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 上有许多高质量的实现。 寻找具有对数递归深度的。

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

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-左右,编译器就会耗尽内存。这种大小的构建非常慢,但合理的大小(例如 1024(似乎没问题。

最新更新