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++17 详解 说明
  • 第一部分——语言特性
  • 1. 快速入门
  • 2. 移除或修正的语言特性
  • 3. 语言澄清(Language Clarification)
  • 4. 通用语言特性
  • 5. 模板(Templates)
    • 5. 模板(Templates)
      • 类模板的模板参数推导
      • 推导指引
      • CTAD的局限性
      • 额外信息
      • 折叠表达式
      • 更多示例
      • 额外信息
      • if constexpr
      • 为什么需要编译期 if?
      • 模板代码简化
      • 示例
      • 额外信息
      • 使用auto声明非类型模板参数
      • 额外信息
      • 其他变更
      • 编译器支持
  • 6. 代码标注
  • 第二部分 - 标准库的变化
  • 7. std::optional
  • 8. std::variant
  • 9. std::any
  • 10. std::string_view
  • 11. 字符串转换
  • 12. 搜索器与字符串匹配
  • 13. 文件系统
  • 14. 并行STL算法
  • 15. 标准库中的其他变化
  • 16. 移除和弃用的库特性
  • 第三部分 - 更多示例和用例
  • 17. 使用std::optional和std::variant进行重构
  • 18. 使用[[nodiscard]]强制执行代码契约
  • 19. 用if constexpr替换enable_if——带可变参数的工厂函数
  • 20. 如何实现CSV读取器的并行化
目录

5. 模板(Templates)

# 5. 模板(Templates)

你使用过模板和(或)元编程(meta-programming)吗?

如果你的答案是 “是”,那么你可能会对C++17中的更新感到满意。

新标准引入了许多改进,使模板编程更加容易,表达能力也更强。

在本章中,你将了解到:

  • 类模板的模板参数推导(Template argument deduction for class templates)
  • template<auto>
  • 折叠表达式(Fold expressions)
  • if constexpr——C++的编译期if语句!
  • 以及一些较小的、详细的改进和修复

# 类模板的模板参数推导

C++17填补了模板推导规则中的一个空白。现在,模板参数推导不仅适用于函数模板,也适用于类模板。这也意味着,许多使用make_Type函数的代码现在可以省略了。

例如,要创建一个std::pair对象,通常这样写会更方便:

auto myPair = std::make_pair(42, "hello world"s);
1

而不是:

std::pair<int, std::string> myPair{42, "hello world"};
1

因为std::make_pair()是一个模板函数,编译器可以执行函数模板参数的推导,所以无需写成:

auto myPair = std::make_pair<int, std::string>(42, "hello world");
1

现在,从C++17开始,符合标准的编译器也可以很好地推导类模板的模板参数类型!

这个特性被称为 “类模板参数推导(Class Template Argument Deduction)”,简称为CTAD。在我们的示例中,现在你可以这样写:

using namespace std::string_literals;
std::pair myPair{42, "hello world"s};     // 自动推导!
1
2

CTAD在拷贝初始化和通过new()分配内存时也同样适用:

auto otherPair = std::pair{42, "Hello"s}; // 同样会被推导
auto ptr = new std::pair{42, "World"s};   // 使用new时
1
2

CTAD可以大幅简化复杂的构造,比如:

// 锁保护:
std::shared_timed_mutex mut;
std::lock_guard<std::shared_timed_mutex> lck(mut);
// 数组:
std::array<int, 3> arr {1, 2, 3};
1
2
3
4
5

现在可以写成:

std::shared_timed_mutex mut;
std::lock_guard lck(mut);
std::array arr { 1, 2, 3 };
1
2
3

注意,不能进行部分推导,你必须指定所有模板参数,或者一个都不指定:

std::tuple t(1, 2, 3);                // 正确:进行推导
std::tuple<int ,int ,int> t(1, 2, 3); // 正确:提供了所有参数
std::tuple<int> t(1, 2, 3);           // 错误:部分推导
1
2
3

有了这个特性,许多make_Type函数可能不再需要,尤其是那些为类 “模拟” 模板推导的函数。

不过,有些工厂函数会执行额外的工作。例如,std::make_shared,它不仅创建shared_ptr,还确保控制块和指向的对象分配在同一内存区域:

// 控制块和int可能在内存的不同位置
std::shared_ptr<int> p(new int {10});
// 控制块和int在同一连续内存段
auto p2 = std::make_shared<int>(10);
1
2
3
4

类模板的模板参数推导是如何工作的呢?让我们进入 “推导指引(Deduction Guides)” 这一领域。

# 推导指引

编译器使用一种名为 “推导指引” 的特殊规则来确定参数类型。

推导指引有两种类型:编译器生成的(隐式生成)和用户定义的。为了理解编译器如何使用这些指引,让我们来看一个简化版的std::array推导指引¹:

template <typename T, typename... U>
array(T, U...) ->
array<enable_if_t<(is_same_v<T, U> && ...), T>, 1 + sizeof ...(U)>;
1
2
3

这种语法看起来像一个带有后置返回类型的模板函数。编译器将这种 “虚构” 的函数视为参数的候选。如果模式匹配,那么从推导中返回找到的类型。

在我们的例子中,当你这样写:

std::array arr {1, 2, 3, 4};
1

那么,假设T和U...参数的类型相同,我们可以构建一个std::array<int, 4>类型的数组对象。

在大多数情况下,你可以依赖编译器生成自动推导指引。它们会为主要类模板的每个构造函数(包括拷贝/移动构造函数)创建。请注意,专门化或部分专门化的类在这里不适用。

如前所述,你也可以编写自定义推导指引。一个经典的例子是推导为std::string而不是const char*:

template <typename T> struct MyType { T str; };
// 自定义推导指引
MyType(const char *) -> MyType<std::string>;
MyType t{"Hello World"}; // 推导为std::string
1
2
3
4

如果没有自定义推导指引,T会被推导为const char*。

另一个自定义推导指引的例子来自重载模式(在关于std::variant的章节中,与std::visit()相关的部分可以了解更多关于这个模式的内容):

template <class... Ts>
struct overload : Ts... { using Ts::operator()...; };
template <class... Ts>
overload(Ts...) -> overload<Ts...>; // 推导指引
1
2
3
4

overload类从其他类Ts...继承,然后公开它们的operator()。这里使用自定义推导指引将可调用类型列表 “转换” 为我们可以从中派生的类列表。

# CTAD的局限性

在C++17中,类模板的模板参数推导有以下局限性:

  • 它不适用于模板聚合类型
  • 推导不包括继承构造函数
  • 它不适用于模板别名

这些局限性将在C++20中通过已被接受的提案P1021 (opens new window)消除。

# 额外信息

CTAD特性是在P0091R3 (opens new window)和P0433 - Deduction Guides in the Standard Library (opens new window)中提出的。

请注意,虽然编译器可能宣称完全支持类模板的模板参数推导,但其相应的标准模板库(STL)实现可能仍然缺少某些STL类型的自定义推导指引。请参阅本章末尾的 “编译器支持” 部分。

# 折叠表达式

C++11引入了可变参数模板(variadic templates),这是一个强大的特性,尤其是当你想要处理函数中数量可变的输入模板参数时。例如,在C++11之前,你必须为一个模板函数编写多个不同的版本(一个版本处理一个参数,另一个版本处理两个参数,再一个版本处理三个参数……)。

不过,当你想要实现像求和(sum)、全部(all)这样的“递归”函数时,可变参数模板仍需要一些额外的代码。你必须指定递归规则。

例如:

auto SumCpp11() {
    return 0;
}

template <typename T1, typename... T>
auto SumCpp11(T1 s, T... ts) {
    return s + SumCpp11(ts...);
}
1
2
3
4
5
6
7
8

而在C++17中,我们可以编写更简单的代码:

template <typename... Args>
auto sum(Args... args) {
    return (args + ... + 0);
}
// 甚至可以这样:
template <typename... Args>
auto sum2(Args... args) {
    return (args + ...);
}
1
2
3
4
5
6
7
8
9

存在以下带有二元运算符(op)的折叠表达式 (opens new window)变体:

表达式 名称 展开形式
(... op e) 一元左折叠 ((e1 op e2) op ...) op eN
(init op ... op e) 二元左折叠 (((init op e1) op e2) op ...) op eN
(e op ...) 一元右折叠 e1 op (... op (eN - 1 op eN))
(e op ... op init) 二元右折叠 e1 op (... op (eN - 1 op (eN op init)))

op 可以是以下32个二元运算符中的任意一个:+ - * / % ^ & | = < > << >> += -= *= /= %= ^= &= |= <<= >>= == != <= >= && || , .* ->* 。在二元折叠中,两个 op 必须相同。

例如,当你编写:

template <typename... Args>
auto sum2(Args... args) {
    return (args + ...); // 对 '+' 进行一元右折叠
}
auto value = sum2(1, 2, 3, 4);
1
2
3
4
5

模板函数会展开为:auto value = 1 + (2 + (3 + 4));

默认情况下,对于空参数包,我们有以下值:

运算符 默认值
&& true
|| false
, void()
其他任何运算符 格式错误的代码

这就是为什么你不能在没有任何参数的情况下调用 sum2(),因为对 + 运算符的一元折叠对于空参数列表没有默认值。

# 更多示例

这里有一个使用折叠表达式实现 printf 的非常不错的示例(P0036R0 (opens new window)):

template <typename... Args>
void FoldPrint(Args&&... args) {
    (std::cout << ... << std::forward<Args>(args)) << '\n ';
}
FoldPrint("hello", 10, 20, 30);
1
2
3
4
5

然而,上面的 FoldPrint 会逐个打印参数,没有任何分隔符。对于上面的函数调用,你会在输出中看到 hello102030。

如果你想要分隔符和更多格式化选项,就必须改变打印方式并使用逗号折叠:

template <typename... Args>
void FoldSeparateLine(Args&&... args) {
    auto separateLine = [](const auto& v) {
        std::cout << v << '\n ';
    };
    (... , separateLine (std::forward<Args>(args))); // 对逗号运算符进行折叠
}
1
2
3
4
5
6
7

使用逗号运算符进行折叠的技巧非常实用。另一个示例是一个特殊版本的 push_back:

template <typename T, typename... Args>
void push_back_vec(std::vector<T>& v, Args&&... args) {
    (v.push_back(std::forward<Args>(args)), ...);
}
std::vector<float> vf;
push_back_vec(vf, 10.5f, 0.7f, 1.1f, 0.89f);
1
2
3
4
5
6

一般来说,折叠表达式能让你编写更简洁、更简短,可能也更易读的代码。

# 额外信息

这项更改是在N4295 (opens new window)和P0036R0 (opens new window)中提出的。

# if constexpr

这可是个重要特性!

C++的编译期 if!

这个特性允许你在编译时根据常量表达式条件舍弃 if 语句的分支。

if constexpr (cond)
    statement1; // 如果cond为假,则舍弃
else
    statement2; // 如果cond为真,则舍弃
1
2
3
4

例如:

template <typename T>
auto get_value(T t) {
    if constexpr (std::is_pointer_v<T>)
        return *t;
    else
        return t;
}
1
2
3
4
5
6
7

if constexpr 有潜力简化大量的模板代码,尤其是在使用标签调度(tag dispatching)、替换失败不是错误(SFINAE)或预处理技术时。

# 为什么需要编译期 if?

一开始,你可能会问,为什么我们需要 if constexpr 以及那些复杂的模板表达式……普通的 if 难道不行吗?

这里有一个代码示例:

template <typename Concrete, typename... Ts>
std::unique_ptr<Concrete> constructArgs(Ts&&... params) {
    if (std::is_constructible_v<Concrete, Ts...>) // 普通 `if`
        return std::make_unique<Concrete>(std::forward<Ts>(params)...);
    else
        return nullptr;
}
1
2
3
4
5
6
7

上面的例程是 std::make_unique 的“升级版”:当参数允许构造被包装的对象时,它返回 std::unique_ptr ;否则返回 nullptr。

下面是一段测试 constructArgs 的简单代码:

class Test {
public:
    Test(int, int) {}
};

int main() {
    auto p = constructArgs<Test>(10, 10, 10); // 3个参数!
}
1
2
3
4
5
6
7
8

这段代码尝试用三个参数构建 Test,但请注意,Test 只有一个接受两个 int 类型参数的构造函数。

编译时,你可能会得到类似这样的编译错误:

在实例化'typename std::_MakeUniq<_Tp>::single_object std::make_unique(_Args&& ...) [with _Tp = Test; _Args = {int, int, int}; typename std::_MakeUniq<_Tp>::single_object = std::unique_ptr<Test, std::default_delete<Test> >]' 中:
main.cpp:8:40:  从'std::unique_ptr<_Tp> constructArgs(Ts&& ...) [with Concrete = Test; Ts = {int, int, int}]' 所需
1
2

让我们试着理解这个错误信息。模板推导之后,编译器编译以下代码:

if (std::is_constructible_v<Concrete, int, int, int>)
    return std::make_unique<Concrete>(10, 10, 10);
else
    return nullptr;
1
2
3
4

在运行时,if 分支不会被执行,因为 is_constructible_v 返回 false,但该分支中的代码仍必须能够编译通过。

这就是为什么我们需要 if constexpr,以便“舍弃”代码,只编译匹配的语句。

要修复这段代码,你必须添加 constexpr:

template <typename Concrete, typename... Ts>
std::unique_ptr<Concrete> constructArgs(Ts&&... params) {
    if constexpr (std::is_constructible_v<Concrete, Ts...>) // 修复!
        return std::make_unique<Concrete>(std::forward<Ts>(params)...);
    else
        return nullptr;
}
1
2
3
4
5
6
7

现在,编译器在编译时计算 if constexpr 条件,对于表达式 auto p = constructArgs<Test>(10, 10, 10);,整个 if 分支将在编译过程的第二步中被“移除”。

确切地说,被舍弃分支中的代码并没有完全从编译阶段移除。只有依赖于条件中使用的模板参数的表达式才不会被实例化。语法必须始终有效。

例如:

template <typename T>
void Calculate(T t) {
    if constexpr (std::is_integral_v<T>) {
        //...
        static_assert(sizeof(int) == 100);
    } else {
        execute(t);
        strange syntax
    }
}
1
2
3
4
5
6
7
8
9
10

在上面这段人为编写的代码中,如果类型 T 是 int,那么 else 分支将被舍弃,这意味着 execute(t) 不会被实例化。但是 strange syntax 这一行仍会被编译(因为它不依赖于 T),这就是为什么你会得到关于这一行的编译错误。

此外,static_assert 也会产生另一个编译错误,该表达式也不依赖于 T,所以它总是会被计算。

# 模板代码简化

在C++17之前,如果根据类型要求有多个版本的算法,你可以使用模板参数替换失败不是错误(SFINAE,Substitution Failure Is Not An Error)或标签调度(tag dispatching)来生成特定的重载决议集。

例如:

// Templates/sfinae_example.cpp
template <typename T>
std::enable_if_t<std::is_integral_v<T>, T> simpleTypeInfo(T t) {
    std::cout << "foo<integral T> " << t << '\n';
    return t;
}

template <typename T>
std::enable_if_t<!std::is_integral_v<T>, T> simpleTypeInfo(T t) {
    std::cout << "not integral \n";
    return t;
}
1
2
3
4
5
6
7
8
9
10
11
12

在上述示例中,我们有两个函数实现,但最终只有一个会进入重载决议集。如果对于类型T,std::is_integral_v为true,那么就会选用上面的函数,第二个函数则会因为SFINAE被排除。

使用标签调度时也会出现同样的情况:

// Templates/tag_dispatching_example.cpp
template <typename T>
T simpleTypeInfoTagImpl(T t, std::true_type) {
    std::cout << "foo<integral T> " << t << '\n';
    return t;
}

template <typename T>
T simpleTypeInfoTagImpl(T t, std::false_type) {
    std::cout << "not integral \n";
    return t;
}

template <typename T>
T simpleTypeInfoTag(T t) {
    return simpleTypeInfoTagImpl(t, std::is_integral<T>{});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

现在,我们不再使用SFINAE,而是为条件生成一个唯一的类型标签:true_type或false_type。根据结果,只会选择一个实现。

现在我们可以使用if constexpr简化这种模式:

template <typename T>
T simpleTypeInfo(T t) {
    if constexpr (std::is_integral_v<T>) {
        std::cout << "foo<integral T> " << t << '\n';
    } else {
        std::cout << "not integral \n";
    }
    return t;
}
1
2
3
4
5
6
7
8
9

编写模板代码变得更加“自然”,不再需要那么多“技巧”。

# 示例

让我们来看几个例子:

  • 行打印机:在本书这部分开头的“快速入门”章节中,你可能已经见过下面这个例子。现在深入了解一下细节,看看代码是如何工作的。
template <typename T>
void linePrinter(const T& x) {
    if constexpr (std::is_integral_v<T>) {
        std::cout << "num: " << x << '\n';
    } else if constexpr (std::is_floating_point_v<T>) {
        const auto frac = x - static_cast<long>(x);
        std::cout << "flt: " << x << ", frac " << frac << '\n';
    } else if constexpr (std::is_pointer_v<T>) {
        std::cout << "ptr, ";
        linePrinter(*x);
    } else {
        std::cout << x << '\n';
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

linePrinter使用if constexpr检查输入类型,并据此输出额外的信息。处理指针类型时会有一个有趣的情况:当检测到指针时,代码会对其解引用,然后递归调用linePrinter。

  • 声明自定义的get<N>函数:结构化绑定表达式适用于所有成员都是公共成员的简单结构体,例如:
struct S {
    int n;
    std::string s;
    float d;
};

S s;
auto [a, b, c] = s;
1
2
3
4
5
6
7
8

然而,如果你有一个自定义类型(包含私有成员),也可以重写get<N>函数,使结构化绑定能够正常工作。以下代码展示了这个思路:

class MyClass {
public:
    int GetA() const { return a; }
    float GetB() const { return b; }
private:
    int a;
    float b;
};

template <std::size_t I>
auto get(MyClass& c) {
    if constexpr (I == 0) return c.GetA();
    else if constexpr (I == 1) return c.GetB();
}

// 支持类似元组接口的特化
namespace std {
    template <>
    struct tuple_size<MyClass> : integral_constant<size_t, 2> {};
    template <>
    struct tuple_element<0,MyClass> { using type = int; };
    template <>
    struct tuple_element<1,MyClass> { using type = float; };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

在上述代码中,你可以将所有功能整合在一个函数中,这是它的优势。也可以通过模板特化来实现:

template <>
auto& get<0>(MyClass &c) { return c.GetA(); }
template <>
auto& get<1>(MyClass &c) { return c.GetB(); }
1
2
3
4

想要了解更多示例,可以阅读“用if constexpr替换std::enable_if”这一章节,以及“结构化绑定”中关于自定义get<N>特化的部分。你还可以查看以下文章:《在C++17中用if constexpr简化代码 (opens new window)》。

# 额外信息

该变更在P0292R2 (opens new window)中被提出。

# 使用auto声明非类型模板参数

这是在各处使用auto策略的另一部分。在C++11和C++14中,你可以使用auto自动推导变量甚至返回类型,此外还有泛型lambda表达式。现在,你还可以用它来推导非类型模板参数。

例如:

template <auto value>
void f() {}

f<10>();      // 推导为int
1
2
3
4

这很有用,因为你不必为非类型参数的类型单独设置一个参数。就像在C++11/14中:

template <typename Type, Type value>
constexpr Type TConstant = value;

constexpr auto const MySuperConst = TConstant<int, 100>;
1
2
3
4

在C++17中,代码更简单一些:

template <auto value>
constexpr auto TConstant = value;

constexpr auto const MySuperConst = TConstant<100>;
1
2
3
4

无需显式写出Type。

许多论文和文章都指出,作为一种高级用法,异构编译时列表就是一个例子:

template <auto... vs>
struct HeterogenousValueList {};

using MyList = HeterogenousValueList<'a', 100, 'b'>;
1
2
3
4

在C++17之前,无法直接声明这样的列表,必须先提供某个包装类。

# 额外信息

该变更在P0127R2 (opens new window)中被提出。在P0127R1 (opens new window)中,你可以找到更多示例和相关论证。

# 其他变更

在C++17中,还有一些与模板相关的语言和库特性值得一提:

  • 在模板模板参数中允许使用typename:在声明模板模板参数时,允许使用typename代替class。普通类型参数可以互换使用这两个关键字,但模板模板参数之前仅限于使用class。更多信息可查看N4051 (opens new window)。
  • 对所有非类型模板参数进行常量求值:移除了对作为非类型模板参数出现的指针、引用以及指向成员的指针的语法限制。更多信息可查看N4268 (opens new window)。
  • 类型特性的变量模板:所有返回::value的类型特性都有对应的_v变量模板。例如:
    • std::is_integral<T>::value 对应 std::is_integral_v<T>;
    • std::is_class<T>::value 对应 std::is_class_v<T>。 这种改进延续了C++14中为返回::type的类型特性添加_t后缀(模板别名)的做法。这样的变更可以显著缩短模板代码。更多信息可查看P0006R0 (opens new window)。
  • using声明中的包扩展:该特性是对可变参数模板和参数包的增强。现在编译器支持在包扩展中使用using关键字:
template <class... Ts> 
struct overloaded : Ts... {
    using Ts::operator()...;
};
1
2
3
4

overloaded类暴露了基类中operator()的所有重载。在C++17之前,要实现相同的结果,需要对参数包使用递归。overloaded模式对std::visit来说是一个非常有用的增强,在“变体”一章的“重载”部分可以了解更多。更多信息可查看P0195 (opens new window)。

  • 逻辑操作元函数:C++17添加了实用的模板元函数:
    • template<class... B> struct conjunction; —— 逻辑与;
    • template<class... B> struct disjunction; —— 逻辑或;
    • template<class B> struct negation; —— 逻辑非。 以下是一个基于提案代码的示例:
template <typename... Ts>
std::enable_if_t<std::conjunction_v<std::is_same<int, Ts>...>>
PrintIntegers(Ts... args) {
    (std::cout <<... << args) << '\n';
}
1
2
3
4
5

上述函数PrintIntegers可以处理数量可变的参数,但所有参数都必须是int类型。这些辅助元函数可以提高高级模板代码的可读性。它们在<type_traits>头文件中可用。更多信息可查看P0013 (opens new window)。

std::void_t转换特性

这是一个非常简单(对于没有实现针对核心语言工作组(CWG)1558问题(针对C++14)修复的编译器,可能需要更复杂的版本。)的元函数,它将类型列表映射为void:

template <class... >
using void_t = void;
1
2

void_t在使用SFINAE(Substitution Failure Is Not An Error,替换失败不是错误)处理格式错误的类型时非常方便。例如,它可用于检测函数重载:

void Compute(int&) {} // 示例函数

template <typename T, typename = void>
struct is_compute_available : std::false_type {};

template <typename T>
struct is_compute_available<T, 
    std::void_t<decltype(Compute(std::declval<T>()))>>
    : std::true_type {};

static_assert(is_compute_available<int&>::value);
static_assert(!is_compute_available<double&>::value);
1
2
3
4
5
6
7
8
9
10
11
12

is_compute_available用于检查针对给定模板参数是否存在Compute()的重载。如果表达式decltype(Compute(std::declval<T>()))有效,编译器将选择模板特化版本。否则,会触发SFINAE,选择主模板。更多信息可查看N3911 (opens new window)。

# 编译器支持

特性 GCC Clang MSVC
类模板的模板参数推导 7.0/8.0²¹ 5.0 VS 2017 15.7
标准库中的推导指南 8.0²² 7.0/开发中²³ VS 2017 15.7
用auto声明非类型模板参数 7.0 4.0 VS 2017 15.7
折叠表达式 6.0 3.9 VS 2017 15.5
if constexpr 7.0 3.9 VS 2017

批注

²¹ GCC 8.0中对类模板的模板参数推导进行了额外改进(P0512R0 (opens new window))。 ²² LibSTDC++的状态页面 (opens new window)中未列出推导指南,因此我们可以认为它们是作为类模板的模板参数推导的一部分实现的。 ²³ LibC++的状态页面 (opens new window)提到,到目前为止,<string>、序列容器、容器适配器和<regex>部分已实现。

上次更新: 2025/04/01, 13:21:34
4. 通用语言特性
6. 代码标注

← 4. 通用语言特性 6. 代码标注→

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