如何推导C++模板参数,特别是模板类的模板构造函数?
摘要:本篇主要是为了记录在编写一个模板类的模板构造函数中遇到的初始化问题,以及针对这个问题展开的相关知识整理,文章就以引发这个问题的代码为标题了。 问题代码 在编写一个代表空间点的模板类 point 时,我打算为它添加一个模板构造函数: 代码 t
本篇主要是为了记录在编写一个模板类的模板构造函数中遇到的初始化问题,以及针对这个问题展开的相关知识整理,文章就以引发这个问题的代码为标题了。
问题代码
在编写一个代表空间点的模板类 point 时,我打算为它添加一个模板构造函数:
代码
template<typename T, std::size_t N>
struct point
{
using value_type = scalar<T>;
value_type _v[N];
point() : _v{ value_type{} } {}
template<typename U>
explicit point(const U (&arr)[N])
{
if constexpr(std::is_same_v<value_type, U>)
memcpy(_v, arr, n * sizeof(value_type));
else
{
for(std::size_t i = 0; i != N; ++i)
_v[i] = static_cast<value_type>(arr[i]);
}
}
};
point<int, 3> pi3({ 0, 1, 2 });
代码中的 scalar 是 这篇 笔记中提到用于类型限制的别名模板,用以排除非数值类型的模板实例化。
template<typename U> point(const U (&arr)[N]) 这个构造函数的意图是 point 只接受长度为 N 的数组进行初始化。
一切看起来没什么问题,但是当我写下这样的初始化代码时,发现代码仍然能够正常通过编译:
代码
point<int, 3> pi3({ 0, 1 });
为什么料想之中的长度限制并没有起作用?
问题分析
分析 point<int, 3> pi3({ 0, 1 }); 这句代码,编译器是如何处理它的:
1. point<int, 3> pi3 指定了 pi3 这个实例的 T 为 int,N 为 3;
2. pi3({ 0, 1 }) 是一个单参数构造语句,尝试匹配接受单个参数的构造函数,匹配到接受数组引用的自定义构造函数 template<typename U> point<int, 3>::point(const U (&arr)[3]);
3. 根据调用参数 { 0, 1 },即 [int, int] 推导 U 为 int,构造函数实例化为 point<int, 3>::point<int>(const int (&arr)[3]);
4. 使用 { 0, 1 } 对一个临时的 int [3] 进行列表初始化,初始化结果为 { 0, 1, 0 },随后传入构造函数。
point 类的模板参数 N 在类的实例化时被指定为 3,在成员模板构造函数实例化期间它是已知的,函数参数推导过程对它没有任何影响,这句代码能够通过编译的根本原因是长度为 3 的数组能够被只有 2 个元素的初始化列表初始化。
而我由于对初始化细节了解不全面,加之模板代码对问题分析有一定的干扰,一时没有抓住本质,写出了这段一厢情愿的代码。
问题解决
解决方法很简单,把数组的维度也作为模板参数参与推导,然后对它进行约束就能实现这个目的了:
代码
template<typename U, std::size_t M, typename = std::enable_if_t<M == N>>
explicit point(const U (&arr)[M])
{
//...
};
int iarr[] = { 0, 1, 2 };
point<int, 3> pi30(iarr);//OK
point<int, 3> pi31({ 0, 1, 2 });//OK
point<int, 3> pi3({ 0, 1 });//无法通过编译
现在数组的维度 M 需要从构造函数的参数推导出来,如果 M 与 N 不相等,构造函数实例化失败。
问题到此就可以结束了,但是不妨来分析一下 ({ ... }) 这种初始化写法。
