跳转到内容

什么是变量模板(variable templates)?

类似于类模板、函数模板,变量模板(variable templates)是为变量定义的模板。

通过变量模板,可以定义一组通用的常量或变量,减轻代码冗余。

场景引入

假如我们在开发一个库(library),里面有一个数学常数$\pi$(变量Pi)。

注:标准库在<numbers>中定义了一系列常用数学常量,包括$\pi$,std::numbers::pi

在定义Pi时,由于不知道库使用者将如何使用库,因此只能作一些假设,如Pidouble型的,并让值尽可能精确。如这样:

constexpr double Pi {3.141592653589793238462643383279502884};

但有可能,库的使用者只需要Pifloat型或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.141592f};
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>的所有引用都将共享同一变量,如果在一处修改了变量的值,其他地方的变量的值也会改变。

将变量模板标记为constconstexpr可以防止这种情况发生。

参考