第19章 非类型模板参数(NTTP)扩展
# 第19章 非类型模板参数(NTTP)扩展
C++模板参数不一定只是类型,也可以是值(非类型模板参数,Non-Type Template Parameter,NTTP)。不过,这些值的类型是有限制的。C++20增加了更多可用于非类型模板参数的类型。现在,浮点值、数据结构(如std::pair<>
和std::array<>
)的对象、简单类,甚至lambda表达式都可以作为模板实参传递。本章将介绍这些类型以及该特性的一些实用应用。
注意,非类型模板参数还有另一项新特性:自C++20起,可以使用概念(concepts)来约束非类型模板参数的值。
# 19.1 非类型模板参数的新类型
自C++20起,你可以将以下新类型用于非类型模板参数:
- 浮点类型(如
double
) - 结构体和简单类(如
std::pair<>
),这也间接允许将字符串字面量用作模板参数 - lambda表达式
实际上,非类型模板参数现在可以是所有结构类型(structural type)。结构类型是指:
- (带
const
或volatile
限定的)算术类型、枚举类型或指针类型; - 左值引用类型;
- 字面量类型(可以是聚合体,或者有一个
constexpr
构造函数,没有复制/移动构造函数、析构函数,且每个数据成员的初始化都是常量表达式),其中:- 所有非静态成员都是公共的、不可变的,并且仅使用结构类型或其数组;
- 所有基类(如果有的话)都是以公共方式继承的,并且也是结构类型。
让我们看看这个新定义会带来哪些影响。
# 19.1.1 浮点值作为非类型模板参数
考虑以下示例:
// lang/nttpdouble.cpp
#include <iostream>
#include <cmath>
template<double Vat>
int addTax(int value) {
return static_cast<int>(std::round(value * (1 + Vat)));
}
int main() {
std::cout << addTax<0.19>(100) << "\n";
std::cout << addTax<0.19>(4199) << "\n";
std::cout << addTax<0.07>(1950) << "\n";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
该程序的输出如下:
119
4997
2087
2
3
通过如下声明addTax()
:
template<double Vat>
int addTax(int value)
2
函数模板addTax()
将一个double
类型的值作为模板参数,然后将其用作增值税,与一个整数值相加。
当非类型模板参数用auto
声明时,现在也允许传递浮点值:
template<auto Vat>
int addTax(int value) {
...
}
2
3
4
std::cout << addTax<0>(1950) << "\n"; // Vat是整数值0
std::cout << addTax<0.07>(1950) << "\n"; // Vat是双精度浮点值0.07
2
同样,现在可以在类模板中使用浮点值(声明为double
或auto
):
template<double Vat>
class Tax {
...
};
2
3
4
# 处理不精确的浮点值
由于舍入误差,浮点类型的值最终会有轻微的不精确性。这在将浮点值用作模板参数时会产生影响。问题在于,两个模板实例化何时具有相同的类型。
考虑以下示例:
// lang/nttpdouble2.cpp
#include <iostream>
#include <limits>
#include <type_traits>
template<double Val>
class MyClass {
};
int main()
{
std::cout << std::boolalpha;
std::cout << std::is_same_v<MyClass<42.0>, MyClass<17.7>> // always false
<< '\n';
std::cout << std::is_same_v<MyClass<42.0>, MyClass<126.0 / 3>> // true or false
<< '\n';
std::cout << std::is_same_v<MyClass<42.7>, MyClass<128.1/ 3>> // true or false
<< "\n\n";
std::cout << std::is_same_v<MyClass<0.1 + 0.3 + 0.00001>,
MyClass<0.3 + 0.1 + 0.00001>> // true or false
<< '\n';
std::cout << std::is_same_v<MyClass<0.1 + 0.3 + 0.00001>,
MyClass<0.00001 + 0.3 + 0.1>> // true or false
<< "\n\n";
constexpr double NaN = std::numeric_limits<double>::quiet_NaN();
std::cout << std::is_same_v<MyClass<NaN>, MyClass<NaN>> // always true
<< '\n';
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
该程序的输出取决于平台。通常如下:
false
true
false
---
true
false
---
true
2
3
4
5
6
7
8
注意,为NaN
实例化的模板总是具有相同的类型,即使NaN == NaN
为false
。
# 19.1.2 对象作为非类型模板参数
自C++20起,如果数据结构或类的所有成员都是公共的,且该类型是字面量类型(literal type),那么就可以将其对象/值用作非类型模板参数。
考虑以下示例:
// lang/nttpstruct.cpp
#include <iostream>
#include <cmath>
#include <cassert>
struct Tax {
double value;
constexpr Tax(double v) : value{v} {
assert(v >= 0 && v < 1);
}
friend std::ostream& operator<< (std::ostream& strm, const Tax& t) {
return strm << t.value;
}
};
template<Tax Vat>
int addTax(int value) {
return static_cast<int>(std::round(value * (1 + Vat.value)));
}
int main() {
constexpr Tax tax{0.19};
std::cout << "tax : " << tax << "\n";
std::cout << addTax<tax>(100) << "\n";
std::cout << addTax<tax>(4199) << "\n";
std::cout << addTax<Tax{0.07}>(1950) << "\n";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
这里,我们声明了一个字面量数据结构Tax
,它有公共成员、一个constexpr
构造函数以及一个额外的成员函数:
struct Tax {
double value;
constexpr Tax(double v) {
...
}
friend std::ostream& operator<< (std::ostream& strm, const Tax& t) {
...
}
};
2
3
4
5
6
7
8
9
这使得我们能够将该类型的对象作为模板参数传递:
constexpr Tax tax{0.19};
std::cout << "tax : " << tax << "\n";
std::cout << addTax<tax>(100) << "\n"; // 将Tax对象作为模板参数传递
2
3
如果数据结构或类是结构类型(structural type),这种做法就行得通。大致来说,结构类型需满足以下条件:
- 所有非静态成员都是公共的,不可变(non - mutable),并且仅使用结构类型或其数组。
- 所有基类(如果有的话)都是以公共方式继承的,并且也是结构类型。
- 该类型是字面量类型(要么是聚合类型,要么有一个
constexpr
构造函数,没有复制/移动构造函数、析构函数,并且数据成员的每次初始化都是常量表达式)。
例如:
// lang/nttpstruct2.cpp
#include <iostream>
#include <array>
constexpr int foo() {
return 42;
}
struct Lit {
int x = foo(); // 没问题,因为foo()是constexpr
int y;
constexpr Lit(int i) // 没问题,因为是constexpr
: y{i} {
}
};
struct Data {
int i;
std::array<double,5> vals;
Lit lit;
};
template<auto Obj>
void func()
{
std::cout << typeid(Obj).name() << "\n";
}
int main() {
func<Data{42, {1, 2, 3}, 42}>(); // 没问题
constexpr Data d2{1, {2}, 3};
func<d2>();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
如果Type
的构造函数或foo()
不是constexpr
,或者使用了std::string
成员,那么Type
就不能被使用。
# std::pair<>和std::array<>的值作为非类型模板参数
因此,现在你可以将std::pair<>
和std::array<>
类型的编译时对象用作模板参数(为此,C++增加了额外的要求,即std::pair<>
和std::array<>
不能用私有基类来实现,在C++20之前,有些实现者是这么做的(见http://wg21.link/lwg3382 (opens new window))):
template<auto Val>
class MyClass {
...
};
MyClass<std::pair{47,11}> mcp; // 自C++20起没问题
MyClass<std::array{0, 8, 15}> mca; // 自C++20起没问题
2
3
4
5
6
7
# 字符串作为非类型模板参数
注意,具有字符数组作为公共成员的数据结构是结构类型。这样,我们现在可以很容易地将字符串字面量作为模板参数传递。
例如:
// lang/nttpstring.cpp
#include <iostream>
#include <string_view>
template<auto Prefix>
class Logger {
...
public:
void log(std::string_view msg) const {
std::cout << Prefix << msg << "\n";
}
};
template<std::size_t N>
struct Str {
char chars[N];
const char* value() {
return chars;
}
friend std::ostream& operator<< (std::ostream& strm, const Str& s) {
return strm << s.chars;
}
};
template<std::size_t N> Str(const char(&)[N]) -> Str<N>; // 推导指引
int main() {
Logger<Str{ "> "}> logger;
logger.log( "hello ");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
该程序的输出如下:
> hello
# 19.1.3 作为非类型模板参数的lambda
由于lambda只是函数对象的简写形式,只要lambda可在编译时使用,它们现在也能用作非类型模板参数。
考虑以下示例:
// lang/nttplambda.cpp
#include <iostream>
#include <cmath>
template<std::invocable auto GetVat>
int addTax(int value)
{
return static_cast<int>(std::round(value * (1 + GetVat())));
}
int main() {
auto getDefaultTax = [] {
return 0.19;
};
std::cout << addTax<getDefaultTax>(100) << '\n';
std::cout << addTax<getDefaultTax>(4199) << '\n';
std::cout << addTax<getDefaultTax>(1950) << '\n';
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
函数模板addTax()
使用了一个辅助函数,该辅助函数现在也可以是一个lambda:
template<std::invocable auto GetVat>
int addTax(int value)
{
return static_cast<int>(std::round(value * (1 + GetVat())));
}
2
3
4
5
我们现在可以将一个lambda传递给这个函数模板:
auto getDefaultTax = [] {
return 0.19;
};
addTax<getDefaultTax>(100) // 将lambda作为模板参数传递
2
3
4
我们甚至可以在调用函数模板时直接定义lambda:
addTax<[]{ return 0.19; }>(100) // 将lambda作为模板参数传递
注意,使用std::invocable
或std::regular_invocable
概念来约束模板参数是个不错的主意。这样,你可以记录并确保传递的可调用对象能够使用指定类型的参数进行调用。
仅使用std::invocable auto
时,我们要求可调用对象不接受参数。如果传递的可调用对象应该接受参数,你需要这样做:
template<std::invocable<std::string> auto GetVat>
int addTax(int value, const std::string& name)
{
double vat = GetVat(name); // 根据传递的名称获取增值税
// ...
}
2
3
4
5
6
注意,在函数模板的声明中不能省略auto
。我们使用std::invocable
作为传递的值/对象/回调的类型约束:
template<std::invocable auto GetVat> // GetVat是具有约束类型的可调用对象
如果没有auto
,我们将声明一个具有普通类型参数的函数模板,并对其进行约束:
template<std::invocable GetVat> // GetVat是一个受约束的类型
如果我们使用lambda的具体类型声明函数模板,它也能正常工作(不过,这意味着我们必须先定义lambda):
auto getDefaultTax = [] {
return 0.19;
};
template<decltype(getDefaultTax) GetVat>
int addTax(int value)
{
return static_cast<int>(std::round(value * (1 + GetVat())));
}
2
3
4
5
6
7
8
9
使用lambda作为非类型模板参数时,请注意以下约束:
- lambda不能捕获任何内容。
- 必须能够在编译时使用该lambda。
幸运的是,自C++17起,任何仅使用对编译时计算有效的特性的lambda都隐式为constexpr
。或者,你可以用constexpr
或consteval
声明lambda,这样如果使用了无效的编译时特性,会在lambda本身报错,而不是在将其用作模板参数时才报错。
# 19.2 补充说明
自第一个C++标准发布以来,对更多非类型模板参数的需求就一直存在。我们在《C++ Templates – The Complete Guide》第一版中已经讨论过这个问题(见http://tmplbook.com (opens new window))。
允许非类型模板参数使用任意字面量类型的提议最初由延斯·毛雷尔(Jens Maurer)在http://wg21.link/n3413 (opens new window)中提出。允许类对象作为非类型模板参数的提议由杰夫·斯奈德(Jeff Snyder)在http://wg21.link/p0732r0 (opens new window)中针对C++20提出。允许浮点值作为非类型模板参数的提议由约尔格·布朗(Jorg Brown)在http://wg21.link/p1714r0 (opens new window)中针对C++20提出。
最终被接受的措辞由杰夫·斯奈德(Jeff Snyder)和路易斯·迪翁(Louis Dionne)在http://wg21.link/p0732r2 (opens new window)中,以及延斯·毛雷尔(Jens Maurer)在http://wg21.link/p1907r1 (opens new window)中制定。