我有以下类,用作通用的"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>
.
有一个需要T0
和Ts
的变种 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(似乎没问题。