类似于类模板、函数模板,变量模板(variable templates)是为变量定义的模板。
通过变量模板,可以定义一组通用的常量或变量,减轻代码冗余。
场景引入
假如我们在开发一个库(library),里面有一个数学常数$\pi$(变量Pi
)。
注:标准库在<numbers>
中定义了一系列常用数学常量,包括$\pi$,std::numbers::pi
。
在定义Pi
时,由于不知道库使用者将如何使用库,因此只能作一些假设,如Pi
是double
型的,并让值尽可能精确。如这样:
constexpr double Pi { 3.141592653589793238462643383279502884 };
但有可能,库的使用者只需要Pi
是float
型或int
型。这时他们可以手动用static_cast<>
转换Pi
的类型,但这种转换会产生性能成本,并且可能影响类型安全(type safety)。如下:
#include < iomanip >
#include < iostream >
#include < limits >
constexpr double Pi{ 3.141592653589793238462643383279502884 };
int main () {
std::cout << " Double Pi: "
<< std:: setprecision (std::numeric_limits< double >::max_digits10)
<< Pi << std::endl;
std::cout << " Float Pi: "
<< std:: setprecision (std::numeric_limits< float >::max_digits10)
<< static_cast <float> (Pi) << std::endl;
std::cout << " Int Pi: " << static_cast <int> (Pi) << std::endl;
}
注:std::numeric_limits<double>::max_digits10
返回双精度浮点数(double
)的最大有效位数,通常是17。
一种改善的方法是,提前创建不同类型的Pi
,但缺点是代码重复、名字冗长,以及依然可能不能满足库使用者的所有需求。如下:
constexpr int PiInt{ 3 };
constexpr float PiFloat{ 3.141592 f };
constexpr double PiDouble{ 3.141592653589793 };
变量模板(variable templates)
一种更好的办法是,使用变量模板(variable templates):
#include < iomanip >
#include < iostream >
#include < limits >
template < typename T>
constexpr T Pi{ static_cast < T > ( 3.141592653589793238462643383279502884 )};
int main () {
std::cout << " Double Pi: "
<< std:: setprecision (std::numeric_limits< double >::max_digits10)
<< Pi <double> << std::endl;
std::cout << " Float Pi: "
<< std:: setprecision (std::numeric_limits< float >::max_digits10)
<< Pi <float> << std::endl;
std::cout << " Int Pi: " << Pi <int> << std::endl;
// will be an integer initialized to 3
auto IntegerPi{Pi <int> };
// will be a float value
auto FloatPi{Pi <float> };
// will be a double value
auto DoublePi{Pi <double> };
}
例子1 与自定义类型一起使用变量模板
#include < iostream >
template < typename T>
constexpr T Pi{ static_cast < T > ( 3.141592653589793238462643383279502884 )};
struct CustomType {
constexpr CustomType ( double initialValue )
: m_value { static_cast <int> (initialValue)} {}
int m_value;
};
int main () {
auto Container{Pi < CustomType > };
std::cout << " Container Value: " << Container . m_value << std::endl;
}
对于变量模板Pi
,使用自定义类型CustomType
实例化它,也就是Pi<CustomType>
,可以理解为首先创建一个CustomType
实例,然后将常量3.14...
传递给CustomType
类的构造函数。
当使用自定义类型实例化变量模板时,编译器首先创建一个该类的新实例(新变量),并将初始化值传递给该类的构造函数。
可以观察到CustomType
类的构造函数被标记为constexpr
,这些动作都在编译时发生。
static_cast<CustomType>(3.14...)
看起来有点怪异,但该转换是合法的,看下面这个不含变量模板的例子:
struct CustomType {
constexpr CustomType ( double initialValue )
: m_value { static_cast <int> (initialValue)} {}
int m_value;
};
int main () {
double value { 3.14 };
auto result{ static_cast < CustomType > (value)};
}
不加static_cast<T>()
可以吗?
不可以,因为我们用的是统一初始化语法(uniform initialization syntax)来初始化变量,如果不加static_cast<>()
,对于Pi<int>
,编译器将报错:error: narrowing conversion of ‘3.1415926535897931e+0’ from ‘double’ to ‘int’ [-Wnarrowing]
。如果不使用static_cast<T>()
。
用传统的转换方式也可以,但我觉得不够“modern”,如下:
template < typename T>
constexpr T Pi{(T) 3.141592653589793238462643383279502884 };
template < typename T>
constexpr T Pi{ T ( 3.141592653589793238462643383279502884 )};
例子2 编译时计算
编译时计算两个整数的和。代码如下:
template < int x, int y>
constexpr int Sum{x + y};
int main () {
int result{Sum < 1 , 2 > };
}
在变量模板中使用的表达式必须在编译时可计算。上面的例子中,模板参数为两个int
整数(非类型参数),变量值是在编译时对这两个整数参数调用operator+(int, int)
运算符的结果。
注意到,上面的例子中,变量模板都被标记为constexpr
,不标可以吗?可以,但可能产生意料之外的行为,所以最好还是标上。如下:
#include < iostream >
template < int x, int y>
int Sum{x + y};
int main () {
++ Sum < 1 , 2 > ;
int result{Sum < 1 , 2 > };
std::cout << " result: " << result << std::endl;
}
将在控制台打印:result: 4
原因是,Sum<1,2>
相当于int Sum<1, 2>{3};
(把这里的Sum<1,2>
看成变量名),虽然我们多次调用Sum<1,2>
,但只会生成一个变量(就像多次用同样的模板参数实例化类模板只会生成一种类实例)。
所以,对Sum<1,2>
的所有引用都将共享同一变量,如果在一处修改了变量的值,其他地方的变量的值也会改变。
将变量模板标记为const
或constexpr
可以防止这种情况发生。
参考