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. 通用语言特性
    • 4. 通用语言特性
      • 结构化绑定声明
      • 语法
      • 修饰符
      • 结构化绑定还是分解声明?
      • 结构化绑定的限制
      • 绑定
      • 示例
      • 额外信息
      • if和switch的初始化语句
      • 额外信息
      • 内联变量
      • 额外信息
      • 常量表达式(constexpr)lambda表达式
      • 额外信息
      • 在lambda表达式中捕获[*this]
      • 从方法返回lambda表达式
      • 额外信息
      • 嵌套命名空间(Nested Namespaces)
      • 通用语言特性
      • 额外信息
      • has_include预处理表达式
      • 额外信息
      • 编译器支持
  • 5. 模板(Templates)
  • 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读取器的并行化
目录

4. 通用语言特性

# 4. 通用语言特性

在本书的这一部分,我们将探讨对C++语言进行的广泛改进,这些改进有可能让你的代码更加简洁且富有表现力。结构化绑定(structured binding)就是这类通用特性的一个完美示例。借助这一特性,你可以利用一种便捷的语法来处理元组(tuple)以及类似元组的表达式。在像Python这样的其他语言中轻而易举就能实现的功能,如今在C++17中也成为了可能。

在本章中,你将学到:

  • 结构化绑定/分解声明
  • 如何为自定义类提供结构化绑定接口
  • if/switch语句中的初始化语句
  • 内联变量(inline variables)及其对仅包含头文件的库的影响
  • 可用于常量表达式(constexpr)上下文的lambda表达式
  • 如何在lambda表达式中正确包装this指针
  • 简化嵌套命名空间(nested namespaces)的使用
  • 如何使用__has_include指令检测头文件是否存在

# 结构化绑定声明

你经常处理元组(tuples)或对组(pairs)吗?

如果没有,或许你应该开始了解这些实用的类型。元组能让你临时组合数据,并且有出色的库支持,而无需创建小型的自定义类型。像结构化绑定这样的语言特性,让代码的表达更加清晰简洁。

假设有一个函数以对组(std::pair)的形式返回两个结果:

std::pair<int, bool> InsertElement(int el) { ... }
1

你可以这样写:

auto ret = InsertElement(...);
1

然后通过ret.first或ret.second来访问返回值。然而,使用.first或.second来访问值,表达性也不强,很容易混淆名称,而且代码可读性较差。另外,你也可以使用std::tie,它能将元组/对组解包到局部变量中:

int index { 0 };
bool flag { false };
std::tie(index, flag) = InsertElement(10);
1
2
3

当你使用std::set::insert(它返回std::pair)时,这样的代码会很有用:

std::set<int> mySet;
std::set<int>::iterator iter;
bool inserted { false };
std::tie(iter, inserted) = mySet.insert(10);
if (inserted)
    std::cout << "Value was inserted\n ";
1
2
3
4
5
6

如你所见,从函数返回多个值这样一个简单的模式,却需要好几行代码来实现。幸运的是,C++17让这变得简单多了!

在C++17中,你可以这样写:

std::set<int> mySet;
auto [iter, inserted] = mySet.insert(10);
1
2

现在,你可以使用具体的变量名,而不是pair.first和pair.second。此外,原本需要三行代码,现在一行就够了,代码也更易读。而且这样的代码更安全,因为iter和inserted在表达式中就完成了初始化。

这种语法被称为结构化绑定表达式(structured binding expression)。

# 语法

结构化绑定的基本语法如下:

auto [a, b, c, ...] = expression;
auto [a, b, c, ...] { expression };
auto [a, b, c, ...] ( expression );
1
2
3

编译器会将a, b, c, ...列表中的所有标识符作为周围作用域中的名称引入,并将它们绑定到由expression表示的对象的子对象或元素上。

在底层,编译器可能会生成如下伪代码:

auto tempTuple = expression;
using a = tempTuple.first;
using b = tempTuple.second;
using c = tempTuple.third;
1
2
3
4

从概念上讲,expression被复制到一个类似元组的对象(tempTuple)中,该对象的成员变量通过a、b和c暴露出来。不过,变量a、b和c不是引用,它们是生成对象成员变量的别名(或绑定)。临时对象有一个由编译器分配的唯一名称。

例如:

std::pair a(0, 1.0f);
auto [x, y] = a;
1
2

x绑定到生成对象中存储的int类型值,该生成对象是a的副本。类似地,y绑定到float类型值。

# 修饰符

有几个修饰符可用于结构化绑定:

  • const修饰符:
const auto [a, b, c, ...] = expression;
1
  • 引用修饰符:
auto& [a, b, c, ...] = expression;
auto&& [a, b, c, ...] = expression;
1
2

例如:

std::pair a(0, 1.0f);
auto& [x, y] = a;
x = 10;  // 可写访问
// a.first现在是10
1
2
3
4

在这个例子中,x绑定到生成对象中的元素,该元素是对a的引用。现在获取元组成员的引用也相当容易:

auto& [ refA, refB, refC, refD ] = myTuple;
1

你还可以在结构化绑定中添加[[attribute]]:

[[maybe_unused]] auto& [a, b, c, ...] = expression;
1

# 结构化绑定还是分解声明?

你可能还见过这个特性的另一个名称:“分解声明(decomposition declaration)”。在标准化过程中,这两个名称都被考虑过,但最终选择了“结构化绑定”。

# 结构化绑定的限制

结构化绑定存在一些限制。它们不能被声明为static或constexpr,也不能用于lambda表达式的捕获列表中。例如:

constexpr auto [x, y] = std::pair(0, 0);
// 生成错误:结构化绑定声明不能是'constexpr'
1
2

绑定的链接属性也不明确。编译器可以自行选择实现方式(因此有些编译器可能允许在lambda表达式中捕获结构化绑定)。幸运的是,由于C++20提案(已被接受):P1091:扩展结构化绑定使其更像变量声明 (opens new window),这些限制可能会被消除。

# 绑定

结构化绑定(Structured Binding)不仅限于元组(tuples),我们可以从以下三种情况进行绑定:

  1. 如果初始化器是一个数组:
// 适用于数组:
double myArray[3] = {1.0, 2.0, 3.0};
auto [a, b, c] = myArray;
1
2
3

在这种情况下,数组会被复制到一个临时对象中,a、b和c指向数组中复制出来的元素。标识符的数量必须与数组中的元素数量匹配。 2. 如果初始化器支持std::tuple_size<>,提供get<N>(),并且还公开了std::tuple_element函数:

std::pair myPair(0, 1.0f);
auto [a, b] = myPair; // 绑定到myPair.first/myPair.second
1
2

在上面的代码片段中,我们对myPair进行绑定。这也意味着,如果为自己的类添加get<N>接口实现,那么你的类也可以支持结构化绑定。后面的部分会有相关示例。 3. 如果初始化器的类型仅包含非静态数据成员:

struct Point {
    double x;
    double y;
};

Point GetStartPoint() {
    return {0.0, 0.0};
}

const auto [x, y] = GetStartPoint();
1
2
3
4
5
6
7
8
9
10

x和y分别指向Point结构体中的Point::x和Point::y。这个类不一定是普通旧数据(POD)类型,但标识符的数量必须等于非静态数据成员的数量。并且这些成员必须在给定的上下文中是可访问的。

注:在C++17最初的版本中,只要类成员是公共的,就可以使用结构化绑定来绑定它们。但当在友元函数(friend functions)的上下文中,甚至在结构体实现内部访问这些成员时,这可能会成为一个问题。这个问题很快就被认定为一个缺陷,目前在C++17中已经得到修复。详见P0969R0 (opens new window)。

# 示例

本节将展示一些结构化绑定很有用的示例。第一个示例中,我们将使用它来编写更具表现力的代码;下一个示例中,你将看到如何为自己的类提供支持结构化绑定的API。

  • 使用结构化绑定编写更具表现力的代码:如果你有一个元素映射(map),可能知道在其内部,元素是以<const Key, ValueType>对的形式存储的。现在,当遍历该映射的元素时:for (const auto& elem : myMap) { ... },你需要通过elem.first和elem.second来引用键和值。结构化绑定最酷的用例之一,就是可以在基于范围的for循环中使用它:
std::map<KeyType, ValueType> myMap;
// C++14:
for (const auto& elem : myMap) {
    // elem.first - 是键
    // elem.second - 是值
}
// C++17:
for (const auto& [key,val] : myMap) {
    // 直接使用键/值
}
1
2
3
4
5
6
7
8
9
10

在上面的示例中,我们绑定到[key, val]对,这样就可以在循环中使用这些名称。在C++17之前,你必须操作从映射返回的迭代器,它返回的是<first, second>对。使用真实的名称key和value更具表现力。 上述技术可以在General Language Features/city_map_iterate.cpp中使用:

#include <map>
#include <iostream> 
#include <string>

int main() {
    const std::map<std::string, int> mapCityPopulation {
        { "Beijing", 21'707'000 },
        { "London", 8'787'892 },
        { "New York", 8'622'698 }
    };
    for (auto&[city, population] : mapCityPopulation)
        std::cout << city << ": " << population << '\n';
}
1
2
3
4
5
6
7
8
9
10
11
12
13

在循环体中,你可以安全地使用city和population变量。

  • 为自定义类提供结构化绑定接口:如前所述,你可以为自定义类提供结构化绑定支持。要做到这一点,你必须为自己的类型定义get<N>、std::tuple_size和std::tuple_element特化。例如,如果你有一个包含三个成员的类,但只想公开其公共接口:
// General Language Features/custom_structured_bindings.cpp
class UserEntry {
public:
    void Load() {}
    std::string GetName() const { return name; }
    unsigned GetAge() const { return age; }
private:
    std::string name;
    unsigned age { 0 };
    size_t cacheEntry { 0 }; // 不公开
};
1
2
3
4
5
6
7
8
9
10
11

结构化绑定的接口:

// General Language Features/custom_structured_bindings.cpp
// 使用if constexpr:
template <size_t I> auto get(const UserEntry& u) {
    if constexpr (I == 0) return u.GetName();
    else if constexpr (I == 1) return u.GetAge();
}

namespace std {
    template <> struct tuple_size<UserEntry> : integral_constant<size_t, 2> {};
    template <> struct tuple_element<0,UserEntry> { using type = std::string; };
    template <> struct tuple_element<1,UserEntry> { using type = unsigned; };
}
1
2
3
4
5
6
7
8
9
10
11
12

tuple_size指定了可用字段的数量,tuple_element定义了特定元素的类型,get<N>返回相应的值。

或者,你也可以使用显式的get<>特化,而不是if constexpr:

template <> string get<0>(const UserEntry &u) { return u.GetName(); }
template <> unsigned get<1>(const UserEntry &u) { return u.GetAge(); }
1
2

对于很多类型来说,编写两个(或几个)函数可能比使用if constexpr更简单直接。

现在你可以在结构化绑定中使用UserEntry,例如:

UserEntry u; 
u.Load();
auto [name, age] = u; // 只读访问
std::cout << name << ", " << age << '\n';
1
2
3
4

这个示例只允许对类进行只读访问。如果你想要可写访问,那么类还应该提供返回成员引用的访问器。之后你还得实现支持引用的get函数。

本节代码使用了if constexpr,你可以在下一章《模板:if constexpr》中了解更多关于这个强大特性的内容。

# 额外信息

该变更在P0217 (opens new window)(措辞)、P0144 (opens new window)(论证和示例)、P0615 (opens new window)(将“分解声明(decomposition declaration)”重命名为“结构化绑定声明(structured binding declaration)”)中被提出。

# if和switch的初始化语句

C++17提供了if和switch语句的新版本:

if (init; condition)
1

和

switch (init; condition)
1

在init部分,你可以像在for循环的init部分一样,指定一个新变量。然后在condition部分检查这个变量。该变量仅在if/else作用域内可见。

在C++17之前,为了实现类似的结果,你必须这样写:

{
    auto val = GetValue();
    if (condition(val))
        // 成功时
    else
        // 失败时...
}
1
2
3
4
5
6
7

请注意,val有一个单独的作用域,否则它会“泄漏”到外部作用域。现在,在C++17中,你可以这样写:

if (auto val = GetValue(); condition(val))
    // 成功时
else
    // 失败时...
1
2
3
4

现在,val仅在if和else语句内部可见,所以它不会“泄漏”。condition可以是任何布尔条件。

这有什么用呢?假设你想在一个字符串中搜索一些内容:

const std::string myString = "My Hello World Wow";
const auto pos = myString.find("Hello");
if (pos != std::string::npos)
    std::cout << pos << " Hello\n";
const auto pos2 = myString.find("World");
if (pos2 != std::string::npos)
    std::cout << pos2 << " World\n";
1
2
3
4
5
6
7

你必须为pos使用不同的名称,或者用单独的作用域将其括起来:

{
    const auto pos = myString.find("Hello");
    if (pos != std::string::npos)
        std::cout << pos << " Hello\n";
}
{
    const auto pos = myString.find("World");
    if (pos != std::string::npos)
        std::cout << pos << " World\n";
}
1
2
3
4
5
6
7
8
9
10

新的if语句可以在一行内实现额外的作用域:

if (const auto pos = myString.find("Hello"); pos != std::string::npos)
    std::cout << pos << " Hello\n";
if (const auto pos = myString.find("World"); pos != std::string::npos)
    std::cout << pos << " World\n";
1
2
3
4

如前所述,在if语句中定义的变量在else块中也可见。所以你可以这样写:

if (const auto pos = myString.find("World"); pos != std::string::npos)
    std::cout << pos << " World\n";
else
    std::cout << pos << " not found!!\n";
1
2
3
4

此外,你可以将它与结构化绑定一起使用(参考赫伯·萨特(Herb Sutter)的代码 (opens new window)):

// 结构化绑定 + if初始化器 配合得更好:
if (auto [iter, succeeded] = mymap.insert(value); succeeded) {
    use(iter);  // 没问题
    // ...
} // iter和succeeded在这里被销毁
1
2
3
4
5

在上面的示例中,你可以引用iter和succeeded,而不是mymap.insert返回的pair.first和pair.second。

如你所见,结构化绑定和元组允许你在if语句的初始化部分创建更多变量。但这样写代码会更易读吗?例如:

string str = "Hi World";
if (auto [pos, size] = pair(str.find("Hi"), str.size()); pos != string::npos)
    std::cout << pos << " Hello, size is " << size;
1
2
3

有人可能会认为,在初始化部分放入更多代码会使代码可读性变差,所以在这种情况下要多加注意。

# 额外信息

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

# 内联变量

自C++11引入非静态数据成员初始化后,现在可以在一个地方声明并初始化成员变量:

class User {
    int _age {0};
    std::string _name {"unknown"};
};
1
2
3
4

然而,对于静态变量(或const static变量),需要先声明,然后在实现文件中进行定义。

C++11中的constexpr关键字允许在一个地方声明并定义静态变量,但仅限于常量表达式。

以前,只有方法/函数可以指定为inline(内联),但现在在头文件中,变量也可以这样做。

根据提案P0386R2 (opens new window):

声明为inline的变量与声明为inline的函数具有相同的语义:它可以在多个翻译单元中进行相同的定义,并且在使用它的每个翻译单元中都必须进行定义,程序的行为就好像只有一个这样的变量。

例如:

// 在头文件中:
struct MyClass {
    static const int sValue; 
};
// 稍后在同一个头文件中:
inline int const MyClass::sValue = 777;
1
2
3
4
5
6

甚至可以在一个地方进行声明和定义:

struct MyClass {
    inline static const int sValue = 777; 
};
1
2
3

另外,注意constexpr变量隐式地是inline的,所以无需写成constexpr inline myVar = 10;。

内联变量比constexpr变量更灵活,因为它不必用常量表达式进行初始化。例如,可以用rand()初始化一个内联变量,但constexpr变量无法这样做 。

它如何简化代码呢? 许多仅包含头文件的库可以减少使用技巧(比如使用内联函数或模板)的情况,转而使用内联变量。

例如:

class MyClass {
    static inline int Seed(); // 静态方法
};
inline int MyClass::Seed() {
    static const int seed = rand();
    return seed;
}
1
2
3
4
5
6
7

可以改为:

class MyClass {
    static inline int seed = rand();
};
1
2
3

C++17保证MyClass::seed在所有编译单元中都具有相同的值(在运行时生成)!

# 额外信息

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

# 常量表达式(constexpr)lambda表达式

lambda表达式在C++11中被引入,从那时起,它已成为现代C++的重要组成部分。C++11的另一个重要特性是constexpr说明符,用于表示函数或值可以在编译时计算。在C++17中,这两个元素可以一起使用,因此你的lambda表达式可以在常量表达式上下文中调用。

在C++11/14中,以下代码无法编译,但在C++17中可以正常运行: Chapter General Language Features/lambda_square.cpp

int main () {
    constexpr auto SquareLambda = [](int n) { return n*n; };
    static_assert(SquareLambda(3) == 9, "");
}
1
2
3
4

自C++17起,遵循标准constexpr函数规则的lambda表达式(其调用运算符operator() )会隐式地声明为constexpr 。

constexpr函数有哪些限制呢?以下是总结(来自10.1.5 constexpr说明符 [dcl.constexpr (opens new window)]:

  • 它们不能是虚函数;
  • 它们的返回类型必须是字面量类型;
  • 它们的参数类型必须是字面量类型;
  • 它们的函数体不能包含:asm定义、goto语句、try块,或者非字面量类型、具有静态或线程存储期的变量。

在实践中,在C++17中,如果你希望函数或lambda表达式在编译时执行,那么该函数体不应调用任何非constexpr的代码。例如,不能动态分配内存或抛出异常。

# 额外信息

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

# 在lambda表达式中捕获[*this]

在类方法中编写lambda表达式时,可以通过捕获this来引用成员变量。例如: Chapter General Language Features/capture_this.cpp

struct Test {
    void foo() {
        std::cout << m_str << '\n ';
        auto addWordLambda = [this]() { m_str += "World"; };
        addWordLambda();
        std::cout << m_str << '\n ';
    }
    std::string m_str {"Hello "};
};
1
2
3
4
5
6
7
8
9

在auto addWordLambda = [this]() {... }这一行中,我们捕获了this指针,随后便可以访问m_str。

请注意,我们通过值捕获了this指针。我们访问的是成员变量,而不是它的副本。当使用[=]或[&]捕获时,效果也是如此。这就是为什么在某个Test对象上调用foo()时,会看到以下输出:

Hello
Hello World
1
2

foo()打印了两次m_str。第一次看到的是“Hello”,但下一次是“Hello World”,因为lambda表达式addWordLambda修改了它。

更复杂的情况会怎样呢?你知道以下代码会发生什么吗?

# 从方法返回lambda表达式

#include <iostream>

struct Baz {
    auto foo() {
        return [=] { std::cout << s << '\n '; };
    }
    std::string s;
};

int main() {
    auto f1 = Baz{"ala"}.foo();
    f1(); 
}
1
2
3
4
5
6
7
8
9
10
11
12
13

这段代码声明了一个Baz对象,然后调用foo()。请注意,foo()返回一个捕获了类成员的lambda表达式。

像[=]这样的捕获块表示我们按值捕获变量,但如果在lambda表达式中访问类的成员,则会隐式地通过this指针进行访问。所以我们捕获了this指针的副本,一旦超出Baz对象的生命周期,这个指针就会成为悬空指针。

在C++17中,可以写成[*this],这将捕获整个对象的副本。auto lam = [*this]() { std::cout << s; };

在C++14中,让代码更安全的唯一方法是使用初始化捕获*this:auto lam = [self=*this] { std::cout << self.s; };

当lambda表达式的生命周期可能比对象本身更长时,捕获this可能会变得很棘手。使用异步调用或多线程时,就可能出现这种情况。

在C++20中(见P0806 (opens new window)),如果在方法中捕获[=],还会看到额外的警告。这样的表达式捕获了this指针,而这可能并非你真正想要的。

# 额外信息

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

# 嵌套命名空间(Nested Namespaces)

命名空间(Namespaces)允许将类型和函数分组到不同的逻辑单元中。

例如,常见的情况是,XY库中的每种类型或函数都会存储在名为xy的命名空间中。就像下面这个例子,有一个SuperCompressionLib库,它提供了Compress()和Decompress()函数:

namespace SuperCompressionLib {
    bool Compress();
    bool Decompress();
}
1
2
3
4

如果你有两个或更多的嵌套命名空间,事情就变得有趣起来。

namespace MySuperCompany {
    namespace SecretProject {
        namespace SafetySystem {
            class SuperArmor {
                // ...
            };
            class SuperShield {
                // ...
            };
        } // SafetySystem
    } // SecretProject
} // MySuperCompany
1
2
3
4
5
6
7
8
9
10
11
12

在C++17中,嵌套命名空间可以用更紧凑的方式书写:

namespace MySuperCompany::SecretProject::SafetySystem {
    class SuperArmor {
        // ...
    };
    class SuperShield {
        // ...
    };
}
1
2
3
4
5
6
7
8

这种语法很方便,对于有C#或Java等语言编程经验的开发者来说,使用起来会更容易。

# 通用语言特性

在C++17中,标准库(Standard Library)的多个地方也通过使用新的嵌套命名空间特性进行了 “简化”。

例如,对于正则表达式(regex)。在C++17中它的定义如下:

namespace std::regex_constants {
    typedef T1 syntax_option_type;
    // ...
}
1
2
3
4

在C++17之前,同样的内容声明如下:

namespace std {
    namespace regex_constants {
        typedef T1 syntax_option_type;
        // ...
    }
}
1
2
3
4
5
6

上述嵌套声明出现在C++规范中,但在标准模板库(STL)的实现中可能看起来有所不同。

# 额外信息

这项改动是在N4230 (opens new window)中提出的。

# has_include预处理表达式

如果你的代码需要在两种不同的编译器下运行,那么你可能会遇到两组不同的可用特性以及特定于平台的变化。

在C++17中,你可以使用has_include预处理常量表达式来检查某个给定的头文件是否存在:

#if has_include(<header_name>)
#if has_include("header_name")
1
2

多年来,has_include在Clang中作为一个扩展已经可用,但现在它被添加到了标准中。它是 “特性测试” 辅助工具的一部分,允许你检查某个特定的C++特性或头文件是否可用。如果一个编译器支持这个宏,那么即使没有启用C++17标志,它也是可访问的。这就是为什么即使你在C++11或C++14 “模式” 下工作,也可以检查某个特性。

例如,我们可以测试某个平台是否有<charconv>头文件,该头文件声明了C++17的底层转换例程: Chapter General Language Features/has_include.cpp

#if defined has_include
#    if has_include(<charconv>)
#        define has_charconv 1
#        include <charconv>
#    endif
#endif

std::optional<int> ConvertToInt(const std::string& str) {
    int value { };
#ifdef has_charconv
    const auto last = str.data() + str.size();
    const auto res = std::from_chars(str.data(), last, value);
    if (res.ec == std::errc{} && res.ptr == last)
        return value;
#else
    // alternative implementation...
#endif

    return std::nullopt;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

在上述代码中,我们根据has_include条件声明了has_charconv。如果头文件不存在,我们需要为ConvertToInt提供一个替代实现。你可以在GCC 7.1和GCC 9.1上检查这段代码,看看效果,因为GCC 7.1没有提供<charconv>头文件。

注意:在上述代码中,我们不能写成:

#if defined has_include && has_include(<charconv>)
1

因为在不支持has_include的旧编译器中,我们会得到一个编译错误。编译器会抱怨由于has_include未定义,整个表达式是错误的。

另一件需要记住的重要事情是,有时编译器可能会提供一个头文件存根。例如,在C++14模式下,<execution>头文件可能存在(它定义了C++17并行算法执行模式),但整个文件可能是空的(由于#ifdef条件)。如果你使用has_include检查该文件并使用C++14模式,那么你会得到错误的结果。

在C++20中,我们将有标准化的特性测试宏,这些宏简化了对各种C++部分的检查。例如,要测试std::any,你可以使用cpp_lib_any;对于lambda表达式支持,有cpp_lambdas。甚至有一个宏可以检查属性支持:has_cpp_attribute(attrib-name)。GCC、Clang和Visual Studio在C++20正式发布之前就已经提供了许多这样的宏。在“Feature testing (C++20) - cppreference (opens new window)”中可以了解更多信息。

has_include以及特性测试宏可以极大地简化多平台代码,这些代码通常需要检查可用的平台元素。

# 额外信息

has_include是在P0061 (opens new window)中提出的。

# 编译器支持

特性 GCC Clang MSVC
结构化绑定声明(Structured Binding Declarations) 7.0 4.0 VS 2017 15.3
if/switch的初始化语句(Init-statement for if/switch) 7.0 3.9 VS 2017 15.3
内联变量(Inline variables) 7.0 3.9 VS 2017 15.5
constexpr lambda表达式(constexpr Lambda Expressions) 7.0 5.0 VS 2017 15.3
lambda捕获*this(Lambda Capture of *this) 7.0 3.9 VS 2017 15.3
嵌套命名空间(Nested namespaces) 6.0 3.6 VS 2015
has_include 5 是 VS 2017 15.3
上次更新: 2025/04/01, 13:21:34
3. 语言澄清(Language Clarification)
5. 模板(Templates)

← 3. 语言澄清(Language Clarification) 5. 模板(Templates)→

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