CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • C++20 完全指南 说明
  • 第1章 比较和<=>运算符
  • 第2章 函数参数的占位符类型
  • 第3章 概念、要求和约束
  • 第4章 概念、需求和约束详解
  • 第5章 标准概念详解
  • 第6章 范围与视图
  • 第7章 范围和视图的实用工具
  • 第8章 视图类型详解
  • 第9章 跨度(Spans)
  • 第10章 格式化输出
  • 第11章 <chrono>中的日期和时区
  • 第12章 std::jthread和停止令牌
  • 第13章 并发特性
  • 第14章 协程
  • 第15章 协程详解
  • 第16章 模块
  • 第17章 Lambda扩展
  • 第18章 编译期计算
  • 第19章 非类型模板参数(NTTP)扩展
    • 19.1 非类型模板参数的新类型
      • 19.1.1 浮点值作为非类型模板参数
      • 处理不精确的浮点值
      • 19.1.2 对象作为非类型模板参数
      • std::pair的值作为非类型模板参数
      • 字符串作为非类型模板参数
      • 19.1.3 作为非类型模板参数的lambda
    • 19.2 补充说明
  • 第20章 新的类型特性
  • 第21章 核心语言的小改进
  • 第22章 泛型编程的小改进
  • 第23章 C++标准库的小改进
  • 第24章 已弃用和移除的特性
  • cpp20completeguides
zhangxf
2025-03-20
目录

第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";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

该程序的输出如下:

119
4997
2087
1
2
3

通过如下声明addTax():

template<double  Vat>
int  addTax(int  value)
1
2

函数模板addTax()将一个double类型的值作为模板参数,然后将其用作增值税,与一个整数值相加。

当非类型模板参数用auto声明时,现在也允许传递浮点值:

template<auto  Vat>
int  addTax(int  value) {
   ...
}
1
2
3
4
std::cout  <<  addTax<0>(1950)  <<  "\n";     // Vat是整数值0
std::cout  <<  addTax<0.07>(1950)  <<  "\n";  // Vat是双精度浮点值0.07
1
2

同样,现在可以在类模板中使用浮点值(声明为double或auto):

template<double  Vat> 
class  Tax  {
   ...
};
1
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';
}
1
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
1
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";
}
1
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)  {
       ...
    } 
};
1
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对象作为模板参数传递
1
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>();
}
1
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起没问题
1
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 ");
}
1
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
1

# 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';
}
1
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())));
}
1
2
3
4
5

我们现在可以将一个lambda传递给这个函数模板:

auto getDefaultTax = [] {
    return 0.19;
};
addTax<getDefaultTax>(100)        // 将lambda作为模板参数传递
1
2
3
4

我们甚至可以在调用函数模板时直接定义lambda:

addTax<[]{ return 0.19; }>(100)   // 将lambda作为模板参数传递
1

注意,使用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);   // 根据传递的名称获取增值税
    // ...
}
1
2
3
4
5
6

注意,在函数模板的声明中不能省略auto。我们使用std::invocable作为传递的值/对象/回调的类型约束:

template<std::invocable auto GetVat>   // GetVat是具有约束类型的可调用对象
1

如果没有auto,我们将声明一个具有普通类型参数的函数模板,并对其进行约束:

template<std::invocable GetVat>        // GetVat是一个受约束的类型
1

如果我们使用lambda的具体类型声明函数模板,它也能正常工作(不过,这意味着我们必须先定义lambda):

auto getDefaultTax = [] {
    return 0.19;
};

template<decltype(getDefaultTax) GetVat>
int addTax(int value)
{
    return static_cast<int>(std::round(value * (1 + GetVat())));
}
1
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)中制定。

上次更新: 2025/03/20, 19:44:38
第18章 编译期计算
第20章 新的类型特性

← 第18章 编译期计算 第20章 新的类型特性→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第四章 Lambda函数
03-27
03
第二章 关键字static及其不同用法
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式