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章 编译期计算
    • 18.1 关键字constinit
      • 18.1.1 在实践中使用constinit
      • 18.1.2 constinit如何解决静态初始化顺序问题
    • 18.2 关键字consteval
      • 18.2.1 第一个consteval示例
      • consteval lambda
      • 18.2.2 constexpr与consteval
      • 18.2.3 在实践中使用consteval
      • consteval的限制
      • 带有consteval函数的调用链
      • 18.2.4 编译时值与编译时上下文
    • 18.3 对constexpr函数放宽的限制
    • 18.4 std::is_constant_evaluated()
      • 18.4.1 std::isconstantevaluated()详解
      • std::isconstantevaluated()与constexpr和consteval
      • std::isconstantevaluated()与运算符?:
    • 18.5 在编译时使用堆内存、向量和字符串
      • 18.5.1 在编译时使用向量
      • 18.5.2 在编译时返回一个集合
      • 18.5.3 在编译时使用字符串
    • 18.6 其他constexpr扩展
      • 18.6.1 constexpr语言扩展
      • 18.6.2 constexpr库扩展
      • constexpr算法和实用函数
      • constexpr库类型
    • 18.7 补充说明
  • 第19章 非类型模板参数(NTTP)扩展
  • 第20章 新的类型特性
  • 第21章 核心语言的小改进
  • 第22章 泛型编程的小改进
  • 第23章 C++标准库的小改进
  • 第24章 已弃用和移除的特性
  • cpp20completeguides
zhangxf
2025-03-20
目录

第18章 编译期计算

# 第18章 编译期计算

本章介绍C++为支持编译期计算而引入的几个扩展特性。

本章涵盖两个新关键字constinit和consteval,以及允许程序员在编译期使用堆内存、向量和字符串的扩展功能。

# 18.1 关键字constinit

C++20引入的一个新关键字是constinit。它可用于强制并确保可变的静态或全局变量在编译期初始化。大致来讲,其效果可以这样描述:

constinit = constexpr - const
1

没错,constinit修饰的变量并非const(或许将这个关键字命名为compiletimeinit会更好)。这个名称源于这些初始化通常发生在编译期常量初始化的时候。

只要声明静态或全局变量,你都可以使用constinit。例如:

// 在任何函数之外:
constinit auto i = 42;

int getNextClassId() {
    static constinit int maxId = 0;
    return ++maxId;
}

class MyType {
    static constinit long max = sizeof(int) * 1000;
   ...
};
1
2
3
4
5
6
7
8
9
10
11
12
constexpr std::array<int, 5> getColl() {
    return {1, 2, 3, 4, 5};
}

constinit auto globalColl = getColl();
1
2
3
4
5

如上述代码所示,你仍然可以修改声明的值。下面这段代码首次使用了上述声明:

std::cout << i << " " << coll[0] << "\n";   // 输出 42  1
i *= 2;
coll = {};
std::cout << i << " " << coll[0] << "\n";   // 输出 84  0
1
2
3
4

输出如下:

42  1
84  0
1
2

使用constinit的效果是,只有当初始值是编译期已知的常量值时,初始化才能通过编译。这意味着,与如下声明不同:

auto x = f();  // f()可能是一个运行时函数
1

使用constinit的对应声明要求在编译期进行初始化,即必须能够在编译期调用f()(这意味着f()必须是constexpr或consteval函数)。

constinit auto x = f(); // f()必须是一个编译期函数
1

如果使用constinit初始化一个对象,那么必须能够在编译期使用其构造函数:

constinit std::pair p{42, "ok"};     // 没问题
constinit std::list l;               // 错误:默认构造函数不是constexpr
1
2

使用constinit的原因如下:

  • 你可以要求在编译期初始化可变的全局/静态对象。这样一来,就可以避免在运行时进行初始化。特别地,在使用thread_local变量时,这有助于提高性能。
  • 你可以确保全局/静态对象在使用时总是已经被初始化。实际上,constinit可用于解决静态初始化顺序问题(static initialization order fiasco),当一个静态/全局对象的初始值依赖于另一个静态/全局对象时,就可能出现这个问题。

注意,使用constinit永远不会改变程序的功能行为(除非存在静态初始化顺序问题),它只会导致代码无法编译通过。

# 18.1.1 在实践中使用constinit

使用constinit时,有几点需要注意2。

首先,不能用另一个constinit修饰的值来初始化constinit变量:

constinit auto x = f(); // f()必须是一个编译期函数
constinit auto y = x;   // 错误:x不是常量初始化器
1
2

原因是初始值必须是编译期已知的常量值,而constinit修饰的值并非常量。只有如下代码可以编译通过:

constexpr auto x = f(); // f()必须是一个编译期函数
constinit auto y = x;   // 没问题
1
2

在初始化对象时,需要使用编译期构造函数,但不要求编译期析构函数。因此,可以将constinit用于智能指针:

constinit std::unique_ptr<int> up;   // 没问题
constinit std::shared_ptr<int> sp;   // 没问题
1
2

constinit并不意味着inline(这与constexpr不同)。例如:

class Type {
    constinit static int val1 = 42;          // 错误
    inline static constinit int val2 = 42;   // 没问题
   ...
};
1
2
3
4
5

可以将constinit与extern一起使用:

// 头文件:
extern constinit int max;

// 翻译单元:
constinit int max = 42;
1
2
3
4
5

为了达到同样的效果,也可以在声明时省略constinit。但是,在定义时省略constinit将不再强制在编译期进行初始化。

可以将constinit与static和thread_local一起使用:

static thread_local constinit int numCalls = 0;
1

constinit、static和thread_local的顺序可以任意。

注意,对于thread_local变量,使用constinit可能会提高性能,因为这可能避免生成的代码需要一个内部保护机制来判断变量是否已经被初始化:

extern thread_local int x1 = 0;
extern thread_local constinit int x2 = 0;   // 更好(可能避免内部保护机制)
1
2

可以使用constinit声明引用,但这没有实际意义,因为引用指向的是常量对象。这种情况下应该使用constexpr。

# 18.1.2 constinit如何解决静态初始化顺序问题

在C++中,存在一个称为静态初始化顺序问题(static initialization order fiasco)的问题,constinit可以解决这个问题。该问题是指不同翻译单元中静态和全局变量的初始化顺序是未定义的。因此,以下代码可能会有问题:

  • 假设有一个带有构造函数的类型用于初始化对象,并声明一个该类型的extern全局对象:
// comptime/truth.hpp
#ifndef TRUTH_HPP
#define TRUTH_HPP

struct Truth {
    int value;
    Truth() : value{42} { // 确保所有对象都初始化为42
    }
};

extern Truth theTruth;    // 声明全局对象

#endif // TRUTH_HPP
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 我们在其所在的翻译单元中初始化该对象:
// comptime/truth.cpp
#include "truth.hpp"
// 定义全局对象(其值应为42)
Truth theTruth;
1
2
3
4
  • 然后在另一个翻译单元中,用theTruth初始化另一个全局/静态对象:
// comptime/fiasco.cpp
#include "truth.hpp"
#include <iostream>

int val = theTruth.value;     // 可能在theTruth初始化之前就被初始化

int main() {
    std::cout << val << "\n"; // 糟糕:可能是0或42
    ++val;
    std::cout << val << "\n"; // 糟糕:可能是1或43
}
1
2
3
4
5
6
7
8
9
10
11

很有可能val在theTruth本身被初始化之前就用theTruth进行了初始化。结果,程序可能会有如下输出(例如,当使用 GCC 编译器时,如果你在将 fiasco.o 传递给链接器之前先传递 truth.o,就会得到这样的效果。):

0
1
1
2

当使用constinit声明val时,这个问题就不会出现。constinit确保对象在使用前总是已经被初始化,因为初始化发生在编译期。如果无法保证这一点,代码将无法编译。注意,如果用constexpr声明val,也能保证初始化;但在这种情况下,就不能再修改该值了。

在我们的示例中,仅使用constinit首先会导致一个编译期错误(提示无法在编译期保证初始化):

// truth.hpp:
struct Truth {
    int value;
    Truth() : value{42} {
    }
};
extern Truth theTruth;

// 主翻译单元:
constinit int val = theTruth.value;   // 错误:没有常量初始化器
1
2
3
4
5
6
7
8
9
10

这个错误信息在编译期提示val无法在编译期被初始化。为使初始化有效,必须修改Truth类和theTruth的声明,以便theTruth能在编译期使用:

// comptime/truthc.hpp
#ifndef TRUTH_HPP
#define TRUTH_HPP

struct Truth {
    int value;
    constexpr Truth() : value{42} {   // 支持编译期初始化
    }
};

constexpr Truth theTruth;             // 强制编译期初始化

#endif // TRUTH_HPP
1
2
3
4
5
6
7
8
9
10
11
12
13

现在,程序可以编译,并且val被保证会用theTruth的初始化值进行初始化:

// comptime/constinit.cpp
#include "truthc.hpp"
#include <iostream>

constinit int val = theTruth.value; // 在theTruth初始化之后被初始化

int main() {
    std::cout << val << "\n";       // 保证输出为42
    ++val;
    std::cout << val << "\n";       // 保证输出为43
}
1
2
3
4
5
6
7
8
9
10
11

因此,现在该程序的输出保证为:

42
43
1
2

还有其他方法可以解决静态初始化顺序混乱的问题(比如使用静态函数获取值,或者使用inline)。尽管如此,如果初始化不需要任何运行时值/特性,你或许应该仔细考虑采用一种总是用constinit声明全局和静态变量的编程风格。在这样的函数中使用constinit至少没有坏处:

long nextId() {
    constinit static long id = 0;
    return ++id;
}
1
2
3
4

# 18.2 关键字consteval

自C++11起,C++就有了关键字constexpr,用于支持在编译时计算函数。如果函数的所有相关信息在编译时都是已知的,那么你也可以在编译时上下文(compile-time context)中使用其结果。不过,constexpr函数也可以作为“普通”的运行时函数使用。

C++20引入了一个类似的关键字consteval,它强制要求在编译时进行计算。与用constexpr标记的函数不同,用consteval标记的函数不能在运行时调用;相反,它们必须在编译时被调用。如果无法在编译时调用,程序就是格式错误的。因为这些函数在编译器看到调用时会立即被调用,所以它们也被称为立即函数(immediate functions) 。

# 18.2.1 第一个consteval示例

考虑以下示例:

// comptime/consteval1.cpp
#include <iostream>
#include <array>

constexpr
bool isPrime(int value) {
    for (int i = 2; i <= value/2; ++i) {
        if (value % i == 0) {
            return false;
        }
    }
    return value > 1;    // 0和1不是质数
}

template<int Num>
consteval
std::array<int, Num> primeNumbers() {
    std::array<int, Num> primes;
    int idx = 0;
    for (int val = 1; idx < Num; ++val) {
        if (isPrime(val)) {
            primes[idx++] = val;
        }
    }
    return primes;
}

int main() {
    // 用质数初始化:
    auto primes = primeNumbers<100>();
    for (auto v : primes) {
        std::cout << v << "\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
28
29
30
31
32
33
34

在这里,我们使用consteval定义了函数primeNumbers<N>(),它在编译时返回前N个质数的数组:

template<int Num>
consteval
std::array<int, Num> primeNumbers() {
    std::array<int, Num> primes;
   ...
    return primes;
}
1
2
3
4
5
6
7

为了计算质数,primeNumbers()使用了一个辅助函数isPrime(),该函数用constexpr声明,以便在运行时和编译时都能使用(我们也可以用consteval声明它,但那样它就不能在运行时使用了) 。

然后,程序使用primeNumbers<>()初始化一个包含100个质数的数组:

auto primes = primeNumbers<100>();
1

因为primeNumbers<>()是consteval函数,所以这个初始化必须在编译时进行。如果primeNumbers<>()用constexpr声明,并且在编译时上下文(比如用于初始化一个constexpr或constinit数组prime)中调用该函数,也会有相同的效果:

template<int Num>
constexpr
std::array<int, Num> primeNumbers() {
    std::array<int, Num> primes;
   ...
    return primes;
}
...
constinit static auto primes = primeNumbers<100>();
1
2
3
4
5
6
7
8
9

在这两种情况下,你都可以看到,当为初始化计算的质数数量显著增加时,编译时间会明显变慢。

不过要注意,编译器在计算常量表达式时通常是有限制的。C++标准仅保证在核心常量表达式中计算1,048,576个表达式。

# consteval lambda

现在你也可以将lambda声明为consteval。这要求该lambda在编译时被求值。

考虑以下示例:

int main(int argc, char* argv[]) {
    // 用于计算字符串字面量哈希值的编译时函数:
    // (有关算法,请参见http://www.cse.yorku.ca/~oz/hash.html)
    auto hashed = [](const char* str) consteval {
        std::size_t hash = 5381;
        while (*str != '\0') {
            hash = hash * 33 ^ *str++;
        }
        return hash;
    };

    // 正确(在编译时上下文需要hashed()):
    enum class drinkHashes : long { beer = hashed("beer"), wine = hashed("wine"), water = hashed("water"), ... };

    // 正确(保证hashed()在编译时被调用):
    std::array arr{hashed("beer"), hashed("wine"), hashed("water")};

    if (argc > 1) {
        switch (hashed(argv[1])) {   // 错误:argv在编译时是未知的
           ...
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

这里我们用一个只能在编译时使用的lambda初始化hashed。因此,对该lambda的使用必须发生在编译时上下文且使用编译时值。只有当调用使用字符串字面量或constexpr const char*类型的参数时才有效。

如果你用constexpr声明该lambda,switch语句就会有效。(用constexpr声明不是必需的,因为自C++17起,所有lambda在可能的情况下都隐式是constexpr的。)然而,这样就不能保证arr初始值的计算发生在编译时了。

有关consteval lambda的更多细节和另一个示例,请参见所有新lambda特性章节中关于consteval lambda的讨论。

# 18.2.2 constexpr与consteval

有了constexpr和consteval,我们现在可以通过以下几种方式影响函数的调用时机:

  • 既不用constexpr也不用consteval:这些函数只能在运行时上下文使用。不过,编译器仍然可以执行在编译时计算它们的优化。
  • constexpr:这些函数可以在编译时和运行时上下文使用。即使在运行时上下文,编译器也仍然可以执行在编译时计算函数的优化。编译器也被允许在运行时计算编译时上下文中的函数。
  • consteval:这些函数只能在编译时使用。不过,其结果可以在运行时上下文使用。

例如,考虑在头文件中声明的以下三个函数(因此,squareR()用inline声明):

// comptime/consteval2.hpp
// 仅用于运行时的square()函数:
inline int squareR(int x) {
    return x * x;
}

// 用于编译时和运行时的square()函数:
constexpr int squareCR(int x) {
    return x * x;
}

// 仅用于编译时的square()函数:
consteval int squareC(int x) {
    return x * x;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

我们可以如下使用这些函数:

// comptime/consteval2.cpp
#include "consteval2.hpp"
#include <iostream>
#include <array>

int main() {
    int i = 42;

    // 在运行时使用运行时值调用square函数:
    std::cout << squareR(i) << "\n";          // 正确
    std::cout << squareCR(i) << "\n";         // 正确
    //std::cout << squareC(i) << ’’;          // 错误

    // 在运行时使用编译时值调用square函数:
    std::cout << squareR(42) << "\n";         // 正确
    std::cout << squareCR(42) << "\n";        // 正确
    std::cout << squareC(42) << "\n";         // 正确:在编译时计算square

    // 在编译时使用square函数:
    //std::array<int, squareR(42)> arr1;      // 错误
    std::array<int, squareCR(42)> arr2;       // 正确:在编译时计算square
    std::array<int, squareC(42)> arr3;        // 正确:在编译时计算square
    //std::array<int, squareC(i)> arr4;       // 错误
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

constexpr和consteval之间的区别如下:

  • consteval函数不允许处理编译时未知的参数:

    std::cout << squareCR(i) << '\n'; // 正确
    std::cout << squareC(i) << '\n';  // 错误
    std::array<int, squareC(i)> arr4; // 错误
    
    1
    2
    3
  • consteval函数必须在编译时执行计算:

std::cout << squareCR(42) << '\n'; // 可能在编译时或运行时计算
std::cout << squareC(42) << '\n';  // 在编译时计算
1
2

这意味着在两种情况下使用consteval是有意义的:

  • 你希望强制进行编译时计算。
  • 你希望禁止函数在运行时使用。

例如,在如下编译时上下文中,函数square()或hashed()是constexpr还是consteval并没有区别:

enum class Drink =  {  water = hashed("water "),  wine = hashed( "wine ")  };
switch (value) {
    case square(42):
        // ...
        break;
}
if constexpr(hashed( "wine ") > hashed( "water ")) {
    // ...
}
1
2
3
4
5
6
7
8
9

然而,在运行时上下文中,consteval可能会产生差异,因为此时并不要求进行编译时计算:

std::array drinks =  {  hashed( "water "),  hashed( "wine ")  };
std::cout << hashed( "water ");
if (hashed( "wine ") > hashed( "water ")) {
    // ...
}
1
2
3
4
5

# 18.2.3 在实践中使用consteval

对于consteval函数,在实际使用时存在一些限制。

# consteval的限制

consteval函数与constexpr函数在大多数其他方面是相同的(注意,C++20对constexpr函数的这些限制有所放宽):

  • 参数和返回类型(如果不是void)必须是字面量类型(literal types)。
  • 函数体中只能包含字面量类型的变量,且这些变量既不能是static的,也不能是thread_local的。
  • 不允许使用goto和标签。
  • 只有当类没有虚基类时,构造函数和析构函数才可以是编译时函数。
  • consteval函数隐式内联。
  • consteval函数不能用作协程。

# 带有consteval函数的调用链

标记为consteval的函数可以调用其他标记为constexpr或consteval的函数:

constexpr int funcConstExpr(int i) {
    return i;
}
consteval int funcConstEval(int i) {
    return i;
}
consteval int foo(int i) {
    return funcConstExpr(i) + funcConstEval(i);   // 正确
}
1
2
3
4
5
6
7
8
9

然而,constexpr函数不能调用consteval函数处理变量:

consteval int funcConstEval(int i) {
    return i;
}
constexpr int foo(int i) {
    return funcConstEval(i);   // 错误
}
1
2
3
4
5
6

函数foo()无法编译。原因是i仍然不是编译时的值,因为它可能在运行时被调用。foo()只能使用编译时变量调用funcConstEval():

constexpr int foo(int i) {
    return funcConstEval(42);   // 正确
}
1
2
3

注意,在这里std::is_constant_evaluated()也无济于事。

标记为consteval的函数也不允许调用纯运行时函数(既未标记constexpr也未标记consteval的函数)。不过,只有在实际执行调用时才会检查这一点。对于consteval函数,只要调用运行时函数的语句不会被执行,包含这些语句就不算错误(这条规则同样适用于在编译时调用的constexpr函数)。

考虑以下示例:

void compileTimeError()    {
}
consteval int nextTwoDigitValue(int val) {
    if (val < 0 || val >= 99) {
        compileTimeError();   // 调用在编译时无效的函数
    }
    return ++val;
}
1
2
3
4
5
6
7
8

这个编译时函数有一个有趣的效果,即它只能用于值在0到98之间的参数:

constexpr int i1 = nextTwoDigitValue(0);    // 正确(将i1初始化为1)
constexpr int i2 = nextTwoDigitValue(98);   // 正确(将i2初始化为99)
constexpr int i3 = nextTwoDigitValue(99);   // 编译时错误
constexpr int i4 = nextTwoDigitValue(-1);   // 编译时错误
1
2
3
4

注意,这里使用static_assert()不起作用,因为它只能用于编译时已知的值,而consteval并不会使函数内部的val成为编译时的值:

consteval int nextTwoDigitValue(int val) {
    static_assert(val >= 0 && val < 99);  // 总是错误:val不是编译时的值
    // ...
}
1
2
3
4

通过使用这个技巧,你可以将编译时函数限制为某些特定的值。这样,你可以在编译时解析字符串时,对无效格式发出信号。

# 18.2.4 编译时值与编译时上下文

你可能会认为编译时函数中的每个值都是编译时值。然而,事实并非如此。在编译时函数中,代码的静态类型和值的动态计算之间也存在差异。

考虑以下示例:

consteval void process() {
    constexpr std::array a1{0, 8, 15};
    constexpr auto n1 = std::ranges::count(a1, 0);   // 正确
    std::array<int, n1> a1b;                         // 正确
    std::array a2{0, 8, 15};
    constexpr auto n2 = std::ranges::count(a2, 0);   // 错误
    std::array a3{0, 8, 15};
    auto n3 = std::ranges::count(a3, 0);             // 正确
    std::array<int, n3> a3b;                         // 错误
}
1
2
3
4
5
6
7
8
9
10

尽管我们处于一个只能在编译时使用的函数中,但a2不是编译时值。因此,它不能用于通常需要编译时值的地方,比如初始化constexpr变量n2。

出于同样的原因,只有n1可以用于声明std::array的大小。使用不是constexpr的n3会失败(即使将n3声明为const也不行)。

如果将process()声明为constexpr函数,情况也是一样。它是否在编译时上下文被调用并不重要。

# 18.3 对constexpr函数放宽的限制

从C++11到C++20,每个C++版本都放宽了对constexpr函数的限制。这也意味着consteval函数现在也有这些限制。

基本上,constexpr和consteval函数现在的限制如下:

  • 参数和返回类型(如果有)必须是字面量类型。
  • 函数体只能定义字面量类型的变量。这些变量既不能是static的,也不能是thread_local的。
  • 不允许使用goto和标签。
  • 只有当类没有虚基类时,构造函数和析构函数才可以是编译时函数。
  • 这些函数不能用作协程。这些函数隐式内联。

# 18.4 std::is_constant_evaluated()

C++20提供了一个新的辅助函数std::is_constant_evaluated(),它允许程序员为编译时和运行时计算实现不同的代码。该函数定义在头文件<type_traits>中(尽管它实际上并不是一个类型函数)。

它允许代码在只能在运行时调用的辅助函数和可以在编译时使用的代码之间进行切换。例如:

// comptime/isconsteval.hpp
#include <type_traits>
#include <cstring>

constexpr int len(const char* s) {
    if (std::is_constant_evaluated()) {
        int idx = 0;
        while (s[idx] != '\0') {           // 适合编译时的代码
            ++idx;
        }
        return idx;
    }
    else {
        return std::strlen(s);             // 运行时调用的函数
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在函数len()中,我们计算原始字符串或字符串字面量的长度。如果在运行时调用,我们使用标准C函数strlen()。然而,为了使该函数也能在编译时使用,如果处于编译时上下文,我们提供了不同的实现。

下面展示这两个分支的调用方式:

constexpr int l1 = len("hello"); // 使用then分支
int l2 = len("hello");			 // 使用else分支(不需要编译时上下文)
1
2

第一次调用len()发生在编译时上下文。在这种情况下,is_constant_evaluated()返回true,因此我们使用then分支。第二次调用len()发生在运行时上下文,所以is_constant_evaluated()返回false,并调用strlen()。即使编译器决定在编译时计算该调用,后者也会发生。关键在于调用是否必须在编译时进行。

下面是一个相反的函数示例:它在编译时和运行时都将整数值转换为字符串:

// comptime/asstring.hpp
#include <string>
#include <format>

// 将整数值转换为std::string
// - 可以在编译时或运行时调用
constexpr std::string asString(long long value) {
    if (std::is_constant_evaluated()) {
        // 编译时版本:
        if (value == 0) {
            return "0 ";
        }
        if (value < 0) {
            return "- " + asString(-value);
        }
        std::string s = asString(value / 10) + std::string(1, value % 10 + '0');
        if (s.size() > 1 && s[0] == '0') { // 如果有前导0则跳过
            s.erase(0, 1);
        }
        return s;
    }
    else {
        // 运行时版本:
        return std::format("{} ", value);
    }
}
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

在运行时,我们简单地使用std::format()。在编译时,我们手动创建一个包含可选负号和所有数字的字符串(我们使用递归方法将数字按正确顺序排列)。在关于将编译时字符串导出为运行时字符串的部分,你可以找到使用该函数的示例。

# 18.4.1 std::is_constant_evaluated()详解

根据C++20标准,当std::is_constant_evaluated()在明显常量求值表达式或转换中被调用时,它返回true。大致在以下情况下会出现这种情况:

  • 在常量表达式中调用。
  • 在常量上下文中调用(在if constexpr、consteval函数或常量初始化中)。
  • 用于可在编译时使用的变量的初始化器中。

例如:

constexpr  bool  isConstEval()  {
    return  std::is_constant_evaluated(); 
}

bool  g1  =  isConstEval();                                     // true
const  bool  g2  =  isConstEval();                             	// true
static  bool  g3  =  isConstEval();                           	// true
static  int  g4  =  g1  +  isConstEval();                   	// false

int main() {
    bool  l1  =  isConstEval();                                 // false
    const  bool  l2  =  isConstEval();                          // true
    static  bool  l3  =  isConstEval();                       	// true
    int  l4  =  g1  +  isConstEval();                           // false
    const  int  l5  =  g1  +  isConstEval();               		// false
    static  int  l6  =  g1  +  isConstEval();              		// false
    int  l7  =  isConstEval()  +  isConstEval();      			// false
    const  auto  l8  =  isConstEval()  +  42  +  isConstEval(); // true 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

如果通过constexpr函数间接调用isConstEval(),我们会得到相同的结果。

# std::is_constant_evaluated()与constexpr和consteval

例如,假设我们定义了以下函数:

bool  runtimeFunc()  {
    return  std::is_constant_evaluated(); // 始终为false
}

constexpr  bool  constexprFunc()  {
    return  std::is_constant_evaluated(); // 可能为false或true
}

consteval  bool  constevalFunc()  {
    return  std::is_constant_evaluated(); // 始终为true
}
1
2
3
4
5
6
7
8
9
10
11

那么我们有以下情况:

void  foo() {
    bool  b1  =  runtimeFunc();               // false
    bool  b2  =  constexprFunc();             // false
    bool  b3  =  constevalFunc();             // true
    static  bool  sb1  =  runtimeFunc();      // false 
    static  bool  sb2  =  constexprFunc();    // true   
    static  bool  ab3  =  constevalFunc();    // true
    const  bool  cb1  =  runtimeFunc();       // 错误
    const  bool  cb2  =  constexprFunc();     // true
    const  bool  cb3  =  constevalFunc();     // true

    int  y  =  42;
    static  bool  sb4  =  y  +  runtimeFunc();  // 函数返回false
    static  bool  sb5  =  y  +  constexprFunc();// 函数返回false
    static  bool  ab6  =  y  +  constevalFunc();// 函数返回true
    const  bool  cb4  =  y  +  runtimeFunc();	// 函数返回false
    const  bool  cb5  =  y  +  constexprFunc();	// 函数返回false
    const  bool  cb6  =  y  +  constevalFunc();	// 函数返回true
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

使用constexpr或static constinit代替const,没有y参与的初始化会产生与cb1、cb2和cb3相同的效果。然而,当涉及y时,使用constexpr或static constinit总是会导致错误,因为涉及到运行时的值。

一般来说,在以下情况下使用std::is_constant_evaluated()没有意义:

  • 作为编译时if的条件,因为这总是返回true:
if  constexpr  (std::is_constant_evaluated())  { // 总是为true
   ...
}
1
2
3

在if constexpr内部,我们处于编译时上下文,这意味着关于我们是否处于编译时上下文这个问题的答案总是true(无论整个函数是从什么上下文被调用的)。

  • 在纯运行时函数内部,因为这通常返回false。 唯一的例外是is_constant_evaluated()用于局部常量求值时:
void  foo()  {
    if  (std::is_constant_evaluated())  {              // 总是为false
       ...
    }
    const  bool  b  =  std::is_constant_evaluated();   // true 
}
1
2
3
4
5
6
  • 在consteval函数内部,因为这总是返回true:
consteval  void  foo()  {
    if  (std::is_constant_evaluated())  {               // 总是为true
       ...
    } 
}
1
2
3
4
5

因此,使用std::is_constant_evaluated()通常只在constexpr函数中有意义。在constexpr函数内部使用std::is_constant_evaluated()来调用consteval函数也没有意义,因为一般情况下不允许从constexpr函数调用consteval函数(C++23可能会通过if consteval使类似这样的代码可行。):

consteval  int  funcConstEval(int  i)  {
    return  i;
}

constexpr  int  foo(int  i)  {
    if  (std::is_constant_evaluated())  {  
        return  funcConstEval(i);   // 错误
    }
    else  {
        return  funcRuntime(i); 
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

# std::is_constant_evaluated()与运算符?:

在C++20标准中,有一个有趣的示例来阐明std::is_constant_evaluated()的使用方式。稍微修改后,代码如下:

int  sz  =  10;
constexpr  bool  sz1  =  std::is_constant_evaluated()  ?  20  :  sz;   // true,所以是20
constexpr  bool  sz2  =  std::is_constant_evaluated()  ?  sz  :  20;   // false,所以报错
1
2
3

这种行为的原因如下:

  • sz1和sz2的初始化要么是静态初始化,要么是动态初始化。
  • 对于静态初始化,初始化器必须是常量。因此,编译器尝试将std::is_constant_evaluated()视为值为true的常量来计算初始化器。
    • 对于sz1,这是成功的。结果是1,这是一个常量。因此,sz1是用20进行常量初始化的。
    • 对于sz2,结果是sz,它不是一个常量。因此,sz2(理论上)是动态初始化的。因此,之前的结果被丢弃,并且在计算初始化器时std::is_constant_evaluated()产生false。因此,初始化sz2的表达式也是20。

然而,sz2不一定是常量,因为在这个计算过程中std::is_constant_evaluated()不一定是常量表达式。因此,用这个20初始化sz2无法编译。

使用const代替constexpr会使情况更加复杂:

int  sz  =  10;
const  bool  sz1  =  std::is_constant_evaluated()  ?  20  :  sz;   // true,所以是20
const  bool  sz2  =  std::is_constant_evaluated()  ?  sz  :  20;   // false,所以也是20
double  arr1[sz1];   // 没问题
double  arr2[sz2];   // 可能编译,也可能不编译
1
2
3
4
5

只有sz1是编译时常量,并且总是可以用于初始化数组。由于上述原因,sz2也被初始化为20。然而,因为初始值不一定是常量,arr2的初始化可能编译,也可能不编译(这取决于所使用的编译器和优化设置)。

# 18.5 在编译时使用堆内存、向量和字符串

从C++20开始,编译时函数可以分配内存,前提是这些内存也在编译时释放。因此,现在可以在编译时使用字符串或向量。不过,有一个重要的限制:编译时创建的字符串或向量不能在运行时使用。原因是编译时分配的内存也必须在编译时释放。

# 18.5.1 在编译时使用向量

下面是一个在编译时使用std::vector<>的示例:

[`comptime/vector.hpp`]
#include  <vector>
#include  <ranges>
#include  <algorithm>
#include  <numeric>

template<std::ranges::input_range  T>
constexpr  auto modifiedAvg(const  T&  rg) {
    using  elemType  =  std::ranges::range_value_t<T>;
    // 用传递范围的元素初始化编译时向量:
    std::vector<elemType>  v{std::ranges::begin(rg), std::ranges::end(rg)};
    // 进行一些修改:
    v.push_back(elemType{});
    std::ranges::sort(v);
    auto  newEnd  =  std::unique(v.begin(),  v.end());
   ...
    // 返回修改后向量的平均值:
    auto  sum  =  std::accumulate(v.begin(),  newEnd, elemType{});
    return  sum  /  static_cast<double>(v.size());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

这里,我们用constexpr定义了modifiedAvg(),这样它就可以在编译时被调用。在函数内部,我们使用std::vector<> v,并用传递范围的元素对其进行初始化。这使我们能够使用向量的完整API(特别是插入和删除元素的功能)。例如,我们插入一个元素,对元素进行排序,并使用unique()删除连续的重复元素。现在所有这些算法都是constexpr的,因此我们可以在编译时使用它们。

不过,最后我们并没有返回向量,只是返回了借助编译时向量计算出的值。

我们可以在编译时调用这个函数:

// comptime/vector.cpp
#include  "vector.hpp"
#include  <iostream>
#include  <array>

int main() {
    constexpr  std::array  orig{0,  8,  15,  132,  4,  77};
    constexpr  auto  avg  = modifiedAvg(orig);
    std::cout  <<  "average :  "  <<  avg  <<  "\n";
}
1
2
3
4
5
6
7
8
9
10

由于avg是用constexpr声明的,modifiedAvg()会在编译时求值。

我们也可以用consteval声明modifiedAvg(),在这种情况下,我们可以按值传递参数,因为在运行时不会复制元素:

template<std::ranges::input_range  T>
consteval  auto modifiedAvg(T  rg)
{
    using  elemType  =  std::ranges::range_value_t<T>;
    // 用传递范围的元素初始化编译时向量:
    std::vector<elemType>  v{std::ranges::begin(rg),
                             std::ranges::end(rg)};
   ...
}
1
2
3
4
5
6
7
8
9

然而,如果一个向量可以在运行时使用,我们仍然不能在编译时声明并初始化它:

int main() {
    constexpr  std::vector  orig{0,  8,  15,  132,  4,  77};   // 错误
   ...
}
1
2
3
4

出于同样的原因,编译时函数只有在返回值在编译时使用的情况下,才能将向量返回给调用者:

// comptime/returnvector.cpp
#include  <vector>

constexpr  auto  returnVector() {
    std::vector<int>  v{0,  8,  15};
    v.push_back(42);
   ...
    return  v;
}

constexpr  auto  returnVectorSize() {
    auto  coll  =  returnVector();
    return  coll.size();
}

int main() {
    //constexpr auto coll = returnVector();  	   // 错误
    constexpr  auto  tmp  =  returnVectorSize();   // 正确
   ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 18.5.2 在编译时返回一个集合

虽然不能返回一个编译时向量以便在运行时使用,但有一种方法可以返回在编译时计算出的元素集合:你可以返回一个std::array<>。唯一的问题是你需要知道数组的大小,因为数组的大小不能由向量的大小来初始化:

std::vector  v;
...
std::array<int,  v.size()>  arr;   // 错误
1
2
3

这样的代码永远无法编译,因为size()是一个运行时值,而arr的声明需要一个编译时值。这段代码是否在编译时求值并不重要。因此,你必须返回一个固定大小的数组。例如:

// comptime/mergevalues.hpp
#include  <vector>
#include  <ranges>
#include  <algorithm>
#include  <array>

template<std::ranges::input_range  T>
consteval  auto mergeValues(T  rg,  auto...  vals) {
    // 从传递的范围创建编译时向量:
    std::vector<std::ranges::range_value_t<T>>  v{std::ranges::begin(rg), std::ranges::end(rg)};
    (... ,  v.push_back(vals));   // 合并所有传递的参数
    std::ranges::sort(v);         // 对所有元素进行排序
    // 将扩展后的集合作为数组返回:
    constexpr  auto  sz  =  std::ranges::size(rg)  +  sizeof...(vals);
    std::array<std::ranges::range_value_t<T>,  sz>  arr{};
    std::ranges::copy(v,  arr.begin());
    return  arr;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

我们在一个consteval函数中使用向量,将可变数量的传递参数与传递范围的元素合并,并对它们进行排序。不过,我们将结果集合作为std::array返回,这样它就可以传递到运行时上下文。例如,下面的程序使用这个函数就没问题:

// comptime/mergevalues.cpp
#include  "mergevalues.hpp"
#include  <iostream>
#include  <array>

int main() {
    // 数组的编译时初始化:
    constexpr  std::array  orig{0,  8,  15,  132,  4,  77,  3};
    // 初始化排序后的扩展数组:
    auto merged  = mergeValues(orig,  42,  4);
    // 打印元素:
    for(const  auto&  i  : merged)  {
        std::cout  <<  i  <<  " ";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

程序输出如下:

0  3  4  4  8  15  42  77  132
1

如果在编译时不知道数组的最终大小,我们就必须声明一个最大大小的返回数组,并额外返回结果的大小。现在合并值的函数可能如下所示:

// comptime/mergevaluessz.hpp
#include  <vector>
#include  <ranges>
#include  <algorithm>
#include  <array>

template<std::ranges::input_range  T>
consteval  auto mergeValuesSz(T  rg,  auto...  vals) {
    // 从传递的范围创建编译时向量:
    std::vector<std::ranges::range_value_t<T>>  v{std::ranges::begin(rg), std::ranges::end(rg)};
    (... ,  v.push_back(vals));   // 合并所有传递的参数
    std::ranges::sort(v);         // 对所有元素进行排序
    // 将扩展后的集合作为数组返回,并返回其大小:
    constexpr  auto maxSz  =  std::ranges::size(rg)  +  sizeof...(vals);
    std::array<std::ranges::range_value_t<T>, maxSz>  arr{};
    auto  res  =  std::ranges::unique_copy(v,  arr.begin());
    return  std::pair{arr,  res.out  -  arr.begin()};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

此外,我们使用std::ranges::unique_copy()在排序后删除连续的重复元素,并返回数组和结果元素的数量。

注意,应该用{}声明arr,以确保数组中的所有值都被初始化。编译时函数不允许产生未初始化的内存。

我们现在可以如下使用返回值:

// comptime/mergevaluessz.cpp
#include  "mergevaluessz.hpp"
#include  <iostream>
#include  <array>
#include  <ranges>

int main() {
    // 数组的编译时初始化:
    constexpr  std::array  orig{0,  8,  15,  132,  4,  77,  3};
    // 初始化排序后的扩展数组:
    auto  tmp  = mergeValuesSz(orig,  42,  4);
    auto merged  =  std::views::counted(tmp.first.begin(),  tmp.second);
    // 打印元素:
    for(const  auto&  i  : merged)  {
        std::cout  <<  i  <<  " ";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

通过使用视图适配器std::views::counted(),我们可以轻松地将返回的数组和返回的元素数量组合成一个单一范围来使用。

现在程序的输出是:

0  3  4  8  15  42  77  132
1

# 18.5.3 在编译时使用字符串

对于编译时字符串,现在所有操作都是constexpr的。因此,现在可以在编译时使用std::string,也可以使用任何其他字符串类型,如std::u8string。

然而,同样存在限制,即不能在运行时使用编译时字符串。例如:

consteval  std::string  returnString() {
    std::string  s  =  "Some  string  from  compile  time ";
   ...
    return  s;
}

void  useString() {
    constexpr  auto  s  =  returnString();   // 错误
   ...
}

constexpr  void  useStringInConstexpr() {
    std::string  s  =  returnString();       // 错误
   ...
}

consteval  void  useStringInConsteval() {
    std::string  s  =  returnString();       // 正确
   ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

你也不能通过仅使用data()、c_str()或将其作为std::string_view来返回编译时字符串以解决这个问题。这样做会返回编译时分配的内存地址。如果发生这种情况,编译器会抛出编译时错误。

不过,我们可以使用与上面向量相同的技巧。我们可以将字符串转换为固定大小的数组,并返回数组和向量的大小。

以下是一个完整的示例:

[`comptime/comptimestring.cpp`]
#include  <iostream>
#include  <string>
#include  <array>
#include  <cassert>
#include  "asstring.hpp"

// 将编译时字符串导出到运行时的函数模板:
template<int  MaxSize>
consteval  auto  toRuntimeString(std::string  s) {
    // 确保导出数组的大小足够大:
    assert(s.size()  <=  MaxSize);
    // 创建一个编译时数组并将所有字符复制到其中:
    std::array<char,  MaxSize+1>  arr{};   // 确保所有元素都被初始化
    for  (int  i  =  0;  i  <  s.size();  ++i)  {
        arr[i]  =  s[i];
    }
    // 返回编译时数组和字符串大小:
    return  std::pair{arr,  s.size()};
}

// 在运行时导入导出的编译时字符串的函数:
std::string  fromComptimeString(const  auto&  dataAndSize) {
    // 用导出的字符数组和大小初始化字符串:
    return  std::string{dataAndSize.first.data(), dataAndSize.second};
}

// 测试函数:
consteval  auto  comptimeMaxStr() {
    std::string  s  =  "max  int  is  "  +  asString(std::numeric_limits<int>::max()) +  "  ( "  +  asString(std::numeric_limits<int>::digits  +  1)
                    +  "  bits) ";
    return  toRuntimeString<100>(s);
}

int main() {
    std::string  s  =  fromComptimeString(comptimeMaxStr());
    std::cout  <<  s  <<  "\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
28
29
30
31
32
33
34
35
36
37
38

同样,我们定义了两个辅助函数,用于将编译时字符串导出为运行时字符串:

  • 编译时函数toRuntimeString()将字符串转换为std::array<>,并返回数组和字符串的大小:
template<int  MaxSize>
consteval  auto  toRuntimeString(std::string  s) {
    assert(s.size()  <=  MaxSize);              // 确保数组大小合适
    // 创建一个编译时数组并将所有字符复制到其中:
    std::array<char,  MaxSize+1>  arr{};   		// 确保所有元素都被初始化
    for  (int  i  =  0;  i  <  s.size();  ++i)  {
        arr[i]  =  s[i];
    }
    return  std::pair{arr,  s.size()};      	// 返回数组和大小
}
1
2
3
4
5
6
7
8
9
10

通过使用assert(),我们再次检查数组的大小是否足够大。同样,我们可以在编译时再次检查,确保不会浪费太多内存。

  • 运行时函数fromComptimeString()获取返回的数组和大小,以初始化一个运行时字符串并返回它:
std::string  fromComptimeString(const  auto&  dataAndSize) {
    return  std::string{dataAndSize.first.data(), dataAndSize.second};
}
1
2
3

函数的测试用例使用了辅助函数asString(),它可以在编译时和运行时将整数值转换为字符串。

例如,该程序的输出如下:

max  int  is  2147483647  (32  bits)
1

# 18.6 其他constexpr扩展

除了能够在编译时使用堆内存之外,自C++20起,编译时函数(无论用constexpr还是consteval声明)还可以使用一些额外的语言特性。因此,此外还有一些库特性现在也能在编译时使用。

# 18.6.1 constexpr语言扩展

自C++20起,以下语言特性可用于编译时函数(无论用constexpr还是consteval声明):

  • 现在可以在编译时使用堆内存。
  • 支持运行时多态性:
    • 现在可以使用虚函数。
    • 现在可以使用dynamic_cast。
    • 现在可以使用typeid。
  • 现在可以使用try-catch块(但仍然不允许抛出异常)。
  • 现在可以更改联合(union)的活动成员。

注意,在constexpr或consteval函数中仍然不允许使用static。

# 18.6.2 constexpr库扩展

C++标准库扩展了可在编译时使用的实用工具。

# constexpr算法和实用函数

<algorithm>、<numeric>和<utility>中的大多数算法现在都是constexpr的。这意味着现在可以在编译时对元素进行排序和累加(见编译时使用向量的示例)。

不过,并行算法(带有执行策略参数的算法)仍然只能在运行时使用。

# constexpr库类型

C++标准库的几种类型现在对constexpr有了更好的支持,使得对象能(更好地)在编译时使用:

  • 现在可以在编译时使用向量和字符串。
  • 一些std::complex<>操作变成了constexpr的。
  • 为std::optional<>和std::variant<>添加了一些缺失的constexpr支持。
  • 为std::invoke()、std::ref()、std::cref()、mem_fn()、not_fn()、std::bind()和std::bind_front()添加了constexpr。
  • 在pointer_traits中,针对原始指针的pointer_to()现在可以在编译时使用。

# 18.7 补充说明

关键字constinit最初由Eric Fiselier在http://wg21.link/p1143r0 (opens new window)中作为一个属性提出。最终被接受的措辞由Eric Fiselier在http://wg21.link/p1143r2 (opens new window)中制定。关键字consteval最初由Richard Smith、Andrew Sutton和Daveed Vandevoorde在http://wg21.link/p1073r0 (opens new window)中提出。最终被接受的措辞由Richard Smith、Andrew Sutton和Daveed Vandevoorde在http://wg21.link/p1073r3 (opens new window)中制定。后来David Stone在http://wg21.link/p1937r2 (opens new window)中进行了一个小的修正。

std::is_constant_evaluated()最初由Daveed Vandevoorde在http://wg21.link/p0595r0 (opens new window)中作为一个constexpr操作符提出。最终被接受的措辞由Richard Smith、Andrew Sutton和Daveed Vandevoorde在http://wg21.link/p0595r2 (opens new window)中制定。

编译期容器(如向量和字符串)最初由Daveed Vandevoorde在http://wg21.link/p0597 (opens new window)中提出。最终被接受的语言特性由Peter Dimov、Louis Dionne、Nina Ranns、Richard Smith和Daveed Vandevoorde在http://wg21.link/p0784r7 (opens new window)中制定。最终被接受的库特性由Antony Polukhin在http://wg21.link/p0858r0 (opens new window)中以及Louis Dionne在http://wg21.link/p1004r2 (opens new window)和http://wg21.link/00980r1 (opens new window)中制定。

上次更新: 2025/03/20, 19:44:38
第17章 Lambda扩展
第19章 非类型模板参数(NTTP)扩展

← 第17章 Lambda扩展 第19章 非类型模板参数(NTTP)扩展→

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