C++17 模板初始化折叠表达式的限制类型-class
C++17 Limit type of fold expression for initialization of a template-class
我基本上尝试编写自己的游戏引擎以供练习和个人使用(我知道,这几乎是一项不可能完成的任务,但正如我所说,这主要是为了学习新事物)。
目前,我正在研究我的数学库(主要是向量和矩阵),我遇到了一个有趣但主要是美学问题。
给出如下伪代码:
template <uint8 size>
struct TVector {
float elements[size];
};
现在我希望能够使用所需数量的浮点数作为参数来构造结构:
TVector<3> vec0(1.0f, 2.5f, -4.0f);
TVector<2> vec1(3.0f, -2.0f);
TVector<3> vec2(2.0f, 2.2f); // Error: arg missing
TVector<2> vec3(1.0f, 2.0f, 3.0f) // Error: too many args
由于数组的大小是由模板参数给定的,我努力为该结构声明一个合适的构造函数。我的最终目标是这样的:
// This is pseudo-ideal-code
TVector(size * (float value)); // Create a constructor with number of size
// parameters, which are all floats
当然,这是非逻辑语法,但我以这种方式取得的最接近的结果是 C++17 倍表达式:
template<typename... Args>
TVector(Args... values) {
static_assert(sizeof...(values) <= size, "Too many args");
uint8 i = 0;
(... , void(elements[i++] = values));
}
它在填充数组方面工作得很好并且(我猜)没有太多开销,但是对于使用这个结构的程序员来说它也容易出错,因为它没有直接指示 构造函数接收了多少个参数。
此外,它没有指定参数的类型,这是我最大的问题。
能用为什么会出问题?
假设有以下使用 TVector 结构的结构:
template <const uint8 rows, const uint8 columns>
struct TMatrix {
// elements[-columns-][-rows-];
TVector<rows> elements[columns];
}
假设构造函数类似于向量结构的折叠表达式,
我希望能够使用相应大小的向量或大括号初始化来构造矩阵。
聚合初始化不起作用。
TVector<2> vec(1.0f, 3.0f);
TMatrix<2, 2> mat0(vec, vec); // Works
TMatrix<2, 2> mat1(vec, {0.2f, -4.2f}); // Error
// Does not compile, because the Type is not clear
给错参数不报错直到编译
(就像一个大小错误的向量,不适合作为矩阵的列)。
错误的来源并不总是很清楚。
TL;DR:现在终于到了我真正的问题:
有没有办法限制折叠表达式的类型,最终完全不使用模板并解决我上面给出的 3 个问题?
我想是这样的:
TVector(float... values) {
// Maybe even specify the size of the pack with the size given in the struct template
uint8 i = 0;
(... , void(elements[i++] = values));
}
并且:
TMatrix(const TVector<rows>&... values) {
uint8 i = 0;
(..., void(elements[i++] = values));
}
当然,我在这里很挑剔,这主要是一个美学问题,但我认为这是一个重要的设计决策,它可以真正提高代码的可用性。
感谢您阅读本文并帮助我解决我的第一个问题:)
使用间接寻址,您可以执行如下操作:
template <typename Seq> struct TVectorImpl;
template <std::size_t, typename T> using force_type = T;
template <std::size_t ... Is>
struct TVectorImpl<std::index_sequence<Is...>>
{
TVectorImpl(force_type<Is, float>... args) : elements{args...} {}
float elements[sizeof...(Is)];
};
template <std::size_t size>
using TVector = TVectorImpl<decltype(std::make_index_sequence<size>())>;
这避免了也有模板方法(因此构造为 {2.4, 5}
有效)。
请注意,它是在 C++14 中(并且 index_sequence
可以在 C++11 中完成)。
因此,在深入研究模板元编程并进行尝试之后,我遇到了一些解决方案(都有自己的小问题)。
std::initializer_list:
优点:
易于实施:
// Constructors:
TVector(std::initalizer_list<float> values);
TMatrix(std::initalizer_list<TVector<rows>> values);
大括号初始化:
TVector<3> vec { 1.0f, 0.0f, 2.0f };
TMatrix<3, 3> mat { vec, { 3.0f, 4.0f, 1.0f }, vec };
缺点:
- 复制开销
- 无法移动值
- 没有指定允许的参数个数
- 我推荐 Andrzej 关于该主题的 C++ 博客:The cost of std::initializer_list
std::array
优点:
易于实施:
// Constructors:
TVector(std::array<float, size>&& values);
TMatrix(std::aray<TVector<rows>, columns>&& values);
如果数组中的对象是可移动的则可移动
缺点:
大括号初始化非常难看
TVector<3> vec { { 1.0f, 0.0f, 2.0f } };
TMatrix<3, 3> mat { vec, TVector<3>{ { 3.0f, 4.0f, 1.0f } }, vec };
折叠表达式 - 我的首选解决方案
优点:
- 无开销
- 可移动
使用统一初始化
TVector<3> vec { 1.0f, 0.0f, 2.0f };
TMatrix<3, 3> mat { vec, TVector<3>{ 3.0f, 4.0f, 1.0f }, vec };
- 可根据构造函数的需要指定
缺点:
- 难以实施和指定
不允许在没有指定类型的情况下嵌套大括号(据我所知)
// Constructors:
template<typename... Args, std::enable_if_t<
is_pack_convertible<float, Args...>::value &&
is_pack_size_of<columns, Args...>::value, bool> = false >
TVector(std::array<float, size>&& values);
template<typename... Args, std::enable_if_t<
is_pack_convertible<Vector<rows>, Args...>::value &&
is_pack_size_of<columns, Args...>::value, bool> = false >
TMatrix(std::aray<TVector<rows>, columns>&& values);
is_pack_convertible / is_pack_size_of
// Declaration - checks if all types of a pack are convertible to one type
template <typename To, typename... Pack> struct is_pack_convertible;
// End of pack case
template <typename To> struct is_pack_convertible<To> : std::true_type {};
// Recursive bool &&
template <typename To, typename From, typename... Pack>
struct is_pack_convertible<To, From, Pack...> {
static constexpr bool value = std::is_convertible<From, To>::value
&& is_pack_convertible<To, Pack...>::value;
};
// Declaration - checks if the pack is equal to a certain size
template <size_t size, typename... Pack> struct is_pack_size_of;
// End of pack: size is equal
template <> struct is_pack_size_of<0> : std::true_type {};
// End of pack: size is not equal
template <size_t remainder> struct is_pack_size_of<remainder> : std::false_type {};
// Count down size for every element in pack
template <size_t size, typename Arg, typename... Pack>
struct is_pack_size_of<size, Arg, Pack...> {
static constexpr bool value = is_pack_size_of<size - 1, Pack...>::value;
};
我希望这对其他人有所帮助,并简要概述了初始化泛型时的选项 类。
我基本上尝试编写自己的游戏引擎以供练习和个人使用(我知道,这几乎是一项不可能完成的任务,但正如我所说,这主要是为了学习新事物)。
目前,我正在研究我的数学库(主要是向量和矩阵),我遇到了一个有趣但主要是美学问题。
给出如下伪代码:
template <uint8 size>
struct TVector {
float elements[size];
};
现在我希望能够使用所需数量的浮点数作为参数来构造结构:
TVector<3> vec0(1.0f, 2.5f, -4.0f);
TVector<2> vec1(3.0f, -2.0f);
TVector<3> vec2(2.0f, 2.2f); // Error: arg missing
TVector<2> vec3(1.0f, 2.0f, 3.0f) // Error: too many args
由于数组的大小是由模板参数给定的,我努力为该结构声明一个合适的构造函数。我的最终目标是这样的:
// This is pseudo-ideal-code
TVector(size * (float value)); // Create a constructor with number of size
// parameters, which are all floats
当然,这是非逻辑语法,但我以这种方式取得的最接近的结果是 C++17 倍表达式:
template<typename... Args>
TVector(Args... values) {
static_assert(sizeof...(values) <= size, "Too many args");
uint8 i = 0;
(... , void(elements[i++] = values));
}
它在填充数组方面工作得很好并且(我猜)没有太多开销,但是对于使用这个结构的程序员来说它也容易出错,因为它没有直接指示 构造函数接收了多少个参数。
此外,它没有指定参数的类型,这是我最大的问题。
能用为什么会出问题?
假设有以下使用 TVector 结构的结构:
template <const uint8 rows, const uint8 columns>
struct TMatrix {
// elements[-columns-][-rows-];
TVector<rows> elements[columns];
}
假设构造函数类似于向量结构的折叠表达式, 我希望能够使用相应大小的向量或大括号初始化来构造矩阵。
聚合初始化不起作用。
TVector<2> vec(1.0f, 3.0f); TMatrix<2, 2> mat0(vec, vec); // Works TMatrix<2, 2> mat1(vec, {0.2f, -4.2f}); // Error // Does not compile, because the Type is not clear
给错参数不报错直到编译 (就像一个大小错误的向量,不适合作为矩阵的列)。
错误的来源并不总是很清楚。
TL;DR:现在终于到了我真正的问题:
有没有办法限制折叠表达式的类型,最终完全不使用模板并解决我上面给出的 3 个问题?
我想是这样的:
TVector(float... values) {
// Maybe even specify the size of the pack with the size given in the struct template
uint8 i = 0;
(... , void(elements[i++] = values));
}
并且:
TMatrix(const TVector<rows>&... values) {
uint8 i = 0;
(..., void(elements[i++] = values));
}
当然,我在这里很挑剔,这主要是一个美学问题,但我认为这是一个重要的设计决策,它可以真正提高代码的可用性。
感谢您阅读本文并帮助我解决我的第一个问题:)
使用间接寻址,您可以执行如下操作:
template <typename Seq> struct TVectorImpl;
template <std::size_t, typename T> using force_type = T;
template <std::size_t ... Is>
struct TVectorImpl<std::index_sequence<Is...>>
{
TVectorImpl(force_type<Is, float>... args) : elements{args...} {}
float elements[sizeof...(Is)];
};
template <std::size_t size>
using TVector = TVectorImpl<decltype(std::make_index_sequence<size>())>;
这避免了也有模板方法(因此构造为 {2.4, 5}
有效)。
请注意,它是在 C++14 中(并且 index_sequence
可以在 C++11 中完成)。
因此,在深入研究模板元编程并进行尝试之后,我遇到了一些解决方案(都有自己的小问题)。
std::initializer_list:
优点:
易于实施:
// Constructors: TVector(std::initalizer_list<float> values); TMatrix(std::initalizer_list<TVector<rows>> values);
大括号初始化:
TVector<3> vec { 1.0f, 0.0f, 2.0f }; TMatrix<3, 3> mat { vec, { 3.0f, 4.0f, 1.0f }, vec };
缺点:
- 复制开销
- 无法移动值
- 没有指定允许的参数个数
- 我推荐 Andrzej 关于该主题的 C++ 博客:The cost of std::initializer_list
std::array
优点:
易于实施:
// Constructors: TVector(std::array<float, size>&& values); TMatrix(std::aray<TVector<rows>, columns>&& values);
如果数组中的对象是可移动的则可移动
缺点:
大括号初始化非常难看
TVector<3> vec { { 1.0f, 0.0f, 2.0f } }; TMatrix<3, 3> mat { vec, TVector<3>{ { 3.0f, 4.0f, 1.0f } }, vec };
折叠表达式 - 我的首选解决方案
优点:
- 无开销
- 可移动
使用统一初始化
TVector<3> vec { 1.0f, 0.0f, 2.0f }; TMatrix<3, 3> mat { vec, TVector<3>{ 3.0f, 4.0f, 1.0f }, vec };
- 可根据构造函数的需要指定
缺点:
- 难以实施和指定
不允许在没有指定类型的情况下嵌套大括号(据我所知)
// Constructors: template<typename... Args, std::enable_if_t< is_pack_convertible<float, Args...>::value && is_pack_size_of<columns, Args...>::value, bool> = false > TVector(std::array<float, size>&& values); template<typename... Args, std::enable_if_t< is_pack_convertible<Vector<rows>, Args...>::value && is_pack_size_of<columns, Args...>::value, bool> = false > TMatrix(std::aray<TVector<rows>, columns>&& values);
is_pack_convertible / is_pack_size_of
// Declaration - checks if all types of a pack are convertible to one type
template <typename To, typename... Pack> struct is_pack_convertible;
// End of pack case
template <typename To> struct is_pack_convertible<To> : std::true_type {};
// Recursive bool &&
template <typename To, typename From, typename... Pack>
struct is_pack_convertible<To, From, Pack...> {
static constexpr bool value = std::is_convertible<From, To>::value
&& is_pack_convertible<To, Pack...>::value;
};
// Declaration - checks if the pack is equal to a certain size
template <size_t size, typename... Pack> struct is_pack_size_of;
// End of pack: size is equal
template <> struct is_pack_size_of<0> : std::true_type {};
// End of pack: size is not equal
template <size_t remainder> struct is_pack_size_of<remainder> : std::false_type {};
// Count down size for every element in pack
template <size_t size, typename Arg, typename... Pack>
struct is_pack_size_of<size, Arg, Pack...> {
static constexpr bool value = is_pack_size_of<size - 1, Pack...>::value;
};
我希望这对其他人有所帮助,并简要概述了初始化泛型时的选项 类。