本期我们接着来介绍C++17的新特性:
相关代码上传至gitee:楼田莉子/Linux学习 - Gitee.com
目录
[价值所在:__has_include 解决了什么问题](#价值所在:__has_include 解决了什么问题)
场景:在Windows上使用DirectSound,在Linux上使用ALSA
[C++11 属性:确立标准语法](#C++11 属性:确立标准语法)
[C++14 属性:实用化开端](#C++14 属性:实用化开端)
[[[deprecated]] 和 [[deprecated("reason")]]](#[[deprecated]] 和 [[deprecated("reason")]])
[C++17 属性:走向精细化控制](#C++17 属性:走向精细化控制)
__had_include
前言
在C++17以前,__has_include并非标准C++的一部分,这给需要依赖特定库或平台特性的项目带来了巨大的兼容性难题。开发者通常采用两类方法来适应不同的编译环境:
-
借助外部构建系统 :最正统的方法是使用
autoconf或CMake这类构建工具。它们在编译前运行测试程序,检测环境中的头文件、库和函数,并据此生成一个config.h头文件,其中定义了如#define HAVE_NVML_H 1的宏。开发者随后通过#ifdef HAVE_NVML_H来编写条件代码。- 缺点:这种方法使得构建流程变得复杂,对于一个快速迭代的小型项目而言,维护构建脚本的成本可能比项目本身还高。
-
使用编译器的非标准扩展 :许多编译器预定义了平台相关的宏,例如
_WIN32(Windows)、__linux__(Linux)等。开发者可以通过检查这些宏来判断当前编译环境。- 缺点:这是一种"间接推断",不仅代码中充斥着难以理解的宏判断,而且在适配新平台或编译器时,需要手动添加新的宏分支,维护成本高。
拥有之后
C++17将__has_include正式纳入标准,使其成为一个原生的预处理常量表达式。它像是一种"如果我包含这个头文件会怎样?"的合法"试探"。
其工作流非常直接:
-
在预处理阶段,编译器遇到
__has_include(header)。 -
编译器会在其标准包含路径中查找指定的头文件。
-
如果文件被成功定位,整个表达式被求值为
1(真);反之则为0(假)。
重要的是,这个过程是零成本的------它只发生在编译期,既不会真的#include该文件,也不会在运行期产生任何代码。
价值所在:__has_include 解决了什么问题
这个特性主要解决了以下三个痛点:
-
标准化跨平台检查 :你不再需要依赖构建系统的探测或编译器专属宏。
__has_include提供了一种统一的、原生的条件编译方式。只要代码是用C++17标准编译的,这个特性就能用。 -
简化构建流程 :对于许多项目,你可以用一行
__has_include来替代复杂的构建系统探测脚本,例如CMake的check_include_file。这让代码的自我描述性更强,减少了对特定构建环境的依赖。 -
实现"尽力而为"的包含:你可以编写优先使用某个高性能或新特性头文件的代码,如果它不可用,则自动退回到一个替代方案。这使得库开发者可以轻松地让自己的库在更广泛的环境下工作,同时利用最新的系统特性。
场景:在Windows上使用DirectSound,在Linux上使用ALSA
这是一个经典的跨平台难题。我们来看C++17前后的解决方案有何不同。
C++17 之前:平台宏判断
这种方法非常脆弱,并且依赖于编译器的平台特定宏定义。
cpp
// 旧式方法:依赖预定义的平台宏
// 问题:宏名因编译器而异,且代码充满平台判断
#ifdef _WIN32
#include <dsound.h> // 仅Windows平台的头文件
#elif defined(__linux__)
#include <alsa/asoundlib.h> // 仅Linux平台的头文件
#else
#error "Unsupported platform"
#endif
C++17 之后:__has_include 方法
这种方法更直接,我们只关心"能否使用这个头文件",而不是"这是什么平台"。
cpp
// 现代方法:直接检查所需功能是否存在
#include <iostream>
#if __has_include(<dsound.h>)
// 如果 <dsound.h> 在系统中存在,则执行此分支
#define USE_DIRECTSOUND
#include <dsound.h>
const char* audio_backend = "DirectSound";
#elif __has_include(<alsa/asoundlib.h>)
// 如果 <alsa/asoundlib.h> 存在,则执行此分支
#define USE_ALSA
#include <alsa/asoundlib.h>
const char* audio_backend = "ALSA";
#else
#define NO_AUDIO_BACKEND
const char* audio_backend = "None";
#endif
int main() {
#if defined(USE_DIRECTSOUND)
std::cout << "正在使用 DirectSound 音频后端。" << std::endl;
#elif defined(USE_ALSA)
std::cout << "正在使用 ALSA 音频后端。" << std::endl;
#else
std::cout << "警告:未找到可用的音频后端!" << std::endl;
#endif
return 0;
}
场景:实现"可选依赖"
对于一个只想使用C++标准库中<filesystem>,并在其不可用时退回到Boost的库,__has_include是最佳选择。
cpp
// 使用 __has_include 实现可选依赖
// 优先检查标准的 <filesystem>
#if __has_include(<filesystem>)
#include <filesystem>
namespace fs = std::filesystem;
#pragma message("Using std::filesystem") // 编译时打印信息
// 如果不可用,则检查 Boost 的 boost/filesystem.hpp
#elif __has_include(<boost/filesystem.hpp>)
#include <boost/filesystem.hpp>
namespace fs = boost::filesystem;
#pragma message("Using Boost.Filesystem as fallback")
#else
#error "This library requires either <filesystem> or <boost/filesystem.hpp>"
#endif
#include <iostream>
int main() {
fs::path p = fs::current_path();
std::cout << "Current path: " << p << std::endl;
return 0;
}
进阶技巧与注意事项
在实际项目中,用好__has_include还需要注意以下几点:
-
检查支持 :在使用前,可以先用
#ifdef __has_include来检查编译器是否支持此特性,以保证向下的兼容性。cpp#ifdef __has_include # if __has_include(<optional>) # include <optional> # define have_optional 1 # endif #endif -
与特性检测宏结合 :
__has_include只保证头文件物理存在 ,不保证其特性被逻辑实现。一个最佳实践是,在包含头文件后,再次使用特性检测宏进行二次确认。cpp#ifdef __has_include # if __has_include(<optional>) # include <optional> # define have_optional 1 # endif #endif -
遵循文件包含规范 :使用尖括号
<>搜索系统头文件 路径,使用双引号""搜索用户自定义文件路径(通常从当前源文件目录开始)。
属性
C++11 属性:确立标准语法
C++11首次引入了标准化属性语法 [[attribute]],结束了各编译器使用 __declspec、__attribute__ 的混乱局面。其设计原则是:编译器应忽略不认识的属性,保证代码可移植。
[[noreturn]]
语义 :标记函数不会正常返回(如通过 throw、abort() 或无限循环终止)。
用途:
-
消除"控制流到达非void函数末尾"的警告。
-
帮助编译器进行死代码消除和调用点优化。
cpp
[[noreturn]] void fatal_error(const char* msg) {
std::cerr << msg << std::endl;
std::abort();
}
int foo(int x) {
if (x < 0) fatal_error("negative!");
return x * 2; // 此处编译器知道fatal_error不会返回,无需警告
}
[[carries_dependency]]
语义 :用于弱一致性内存模型中,指示跨函数传递依赖链。主要用于 std::memory_order_consume 的场景(该特性本身在语言层面因实现问题逐渐被废弃,但仍保留属性)。
用途:极少数硬件架构(如DEC Alpha)上的优化,日常应用极少。可跳过。
C++14 属性:实用化开端
C++14增加了 [[deprecated]],使库作者可以优雅地标记过时实体。
[[deprecated]] 和 [[deprecated("reason")]]
语义 :当程序使用了被标记的实体时,编译器必须发出警告信息,可附带原因。
适用对象:类、非静态数据成员、函数、枚举、typedef、变量、模板特化等。
cpp
[[deprecated("Use new_fun() instead")]]
void old_fun();
void new_fun() { /* ... */ }
int main() {
old_fun(); // 编译器警告:'old_fun' is deprecated: Use new_fun() instead
}
价值:标准化的API演进工具,无需依赖编译器的特殊宏。
C++17 属性:走向精细化控制
C++17是属性演化的重要里程碑,引入了三个极具实践意义的属性,并放宽了属性的附着位置。
[[fallthrough]]
语义 :显式标记 switch 语句中故意省略 break 的穿透行为,消除编译器的穿透警告。
位置 :必须放在要穿透到的下一个 case 标签之前,且自身形成一个语句(即末尾带分号)。
cpp
switch (val) {
case 1:
do_something();
[[fallthrough]]; // 明确告诉编译器:这是有意穿透
case 2:
do_common();
break;
}
意义 :防止了无数因漏写 break 导致的隐性bug,强制开发者表达意图。
[[nodiscard]]
语义 :标记函数的返回值不应被忽略。若调用方忽略返回值,编译器发出警告。
C++20扩展为可带字符串 [[nodiscard("reason")]],且可用于类和枚举。C++17仅支持函数及构造函数。
cpp
[[nodiscard]] int compute_important_value();
struct [[nodiscard]] CriticalResource { ... };
int main() {
compute_important_value(); // 警告:忽略 nodiscard 返回值
CriticalResource{}; // 警告(C++20)
}
价值:将错误检查的责任从文档转移到编译器,极大提升了资源泄露和逻辑错误的发现率。
[[maybe_unused]]
语义 :抑制编译器对未使用实体(变量、函数、类型等)的警告。
用途 :用于断言变量、条件编译中的备用函数、平台特定参数等,比 (void)var 或 #pragma 更优雅标准。
cpp
[[maybe_unused]] int debug_flag = 1; // 即使未使用也不警告
void foo([[maybe_unused]] int x) { // 参数可能被平台条件排除
#ifdef USE_X
use(x);
#endif
}
属性位置的扩展
C++17允许属性出现在更多语法位置:
-
命名空间 :
namespace [[deprecated]] old_ns { ... } -
枚举项 :
enum Color { Red [[deprecated]], Green }; -
using 别名 :
using [[deprecated]] OldPtr = std::shared_ptr<int>;
属性列举
| 属性 | 标准 | 功能说明 | 作用对象 | 典型用法/示例 |
|---|---|---|---|---|
[[noreturn]] |
C++11 | 标记函数不会返回(如通过 abort()、throw、无限循环等)。用于消除"控制流到达非void函数末尾"的警告,并辅助死代码优化。 |
函数 | [[noreturn]] void fatal() { abort(); } |
[[deprecated]] |
C++14 | 标记实体已过时。使用时编译器发出警告。可附带字符串说明替代方案。 | 类、函数、变量、枚举、typedef、模板特化等 | [[deprecated("Use new_func()")]] void old(); |
[[fallthrough]] |
C++17 | 显式标记 switch 中故意省略 break 的穿透行为。消除 -Wimplicit-fallthrough 警告,迫使开发者表达意图。 |
空语句(case 前) |
case 1: foo(); [[fallthrough]]; case 2: bar(); |
[[nodiscard]] |
C++17 (C++20扩展) | 标记返回值不应被忽略。若忽略返回值则编译器警告。C++20后可带字符串说明原因,且可用于类/枚举,禁止丢弃该类型临时对象。 | 函数、构造函数(C++20起可标记类、枚举) | [[nodiscard]] int get_error(); 或 C++20: struct [[nodiscard]] Critical{}; |
[[maybe_unused]] |
C++17 | 抑制"未使用实体"的警告。适用于因条件编译、断言等而可能未使用的变量、函数、类型等。 | 变量、函数、类型、非静态数据成员等 | [[maybe_unused]] int debug = 1; |
[[likely]] |
C++20 | 提示编译器该分支更可能被执行,辅助分支预测优化。 | if/else 或 switch 语句的前置条件 |
if (x > 0) [[likely]] { ... } |
[[unlikely]] |
C++20 | 提示编译器该分支不太可能被执行,辅助分支预测优化。 | if/else 或 switch 语句的前置条件 |
if (err) [[unlikely]] { log(); } |
[[no_unique_address]] |
C++20 | 指示非静态数据成员可以与类内其他成员共享地址(常用于无状态对象、空基类优化替代)。 | 非静态数据成员 | struct S { [[no_unique_address]] Empty e; int i; }; |
[[assume(expr)]] |
C++23 | 向编译器断言 expr 在运行时一定为真,用于优化。如果条件不成立则行为未定义。 |
空语句 | [[assume(x >= 0)]]; |
代码示例
cpp
#include <iostream>
#include <string>
#include <vector>
// ============================================================
// C++ 属性 (attributes) 演变史: C++11 → C++14 → C++17
//
// 属性用 [[...]] 语法告诉编译器额外信息: 警告、优化提示等。
// 与 #pragma 不同,属性是标准化的、跨平台的。
// 编译器对不认识的属性必须忽略 (不报错),保证了向前兼容。
// ============================================================
// ============================================================
// C++11: [[noreturn]] --- 函数不会返回
// ============================================================
// 告诉编译器此函数绝对不返回 (内部会 throw / abort / exit / 死循环)。
// 好处: 编译器可以做更激进的优化,并消除 "missing return" 假警告。
[[noreturn]] void fatal_error(const char* msg) {
std::cerr << "[FATAL] " << msg << '\n';
std::abort(); // 进程终止,不会回到调用方
}
// 如果没有 [[noreturn]],下面这个函数在 C++11 中编译器会警告
// "control reaches end of non-void function",尽管你也知道永远不会走到那里。
int always_throws(int code) {
if (code < 0) {
throw std::runtime_error("negative code");
}
// 这里编译器可能报警,因为看起来没有 return
// 但实际不可能走到这里 (if 永远成立)... 没关系,C++17 有更好的写法
return 0; // 只能加一个虚假的 return 来消除警告
}
// ============================================================
// C++14: [[deprecated]] --- 标记"已废弃,请别用"
// ============================================================
// 使用者调用时会触发编译警告 (不是错误),提示迁移到新接口。
// C++14 还支持附带废弃原因字符串: [[deprecated("reason")]]
// 旧接口,计划下个版本删除
[[deprecated("Use new_api_v2() instead")]]
void old_api() {
std::cout << "old_api called\n";
}
// 新接口替代
void new_api_v2() {
std::cout << "new_api_v2 called\n";
}
// 也可以标记整个类型
struct [[deprecated("Use BetterConfig instead")]] LegacyConfig {
int timeout = 30;
};
struct BetterConfig {
int timeout_ms = 30000;
};
// ============================================================
// C++17: [[fallthrough]] --- 故意不写 break,不是 bug
// ============================================================
// switch-case 中如果不写 break,编译器常警告 possible fallthrough。
// 但有时故意需要 fallthrough (比如状态机)。用 [[fallthrough]] 告诉
// 编译器:"我是故意的,别报警"。
enum class LogLevel { Debug, Info, Warn, Error };
void process_log(LogLevel level) {
switch (level) {
case LogLevel::Error:
std::cout << "[ERROR]\n";
// 不 break ------ Error 级别也输出 Warn 信息
[[fallthrough]]; // C++17: 明确告诉编译器这是故意的
case LogLevel::Warn:
std::cout << "[WARN] preparing alert\n";
[[fallthrough]];
case LogLevel::Info:
// Info 及以上都要做的通用处理
std::cout << " → logging to file\n";
break;
case LogLevel::Debug:
std::cout << "[DEBUG] verbose detail\n";
break;
}
// 规则: [[fallthrough]] 只能放在 case/标签 之前,且后面必须跟
// 下一个 case 或 default,不能放在 switch 末尾。
}
// ============================================================
// C++17: [[nodiscard]] --- 返回值不能忽略
// ============================================================
// 如果调用方忽略了返回值,编译器发出警告。
// 典型场景: 错误码返回时忘了检查、纯函数调用后没用到结果、
// 返回新对象的函数被误当成修改自身的函数。
// 场景 1: 返回错误码,调用方很可能忘了检查
enum class [[nodiscard]] DbError {
Ok = 0, ConnectionLost, Timeout, AuthenticationFailed
};
[[nodiscard]] DbError connect_to_db([[maybe_unused]] const std::string& host) {
// 模拟总是失败 --- host 仅在真实实现中使用
return DbError::Timeout;
}
// 场景 2: 返回新对象 vs 修改自身 --- 常见的 bug 来源
class StringBuffer {
std::string data_;
public:
explicit StringBuffer(std::string s) : data_(std::move(s)) {}
// append 修改自身,返回 void ------ 不会误用
void append(const std::string& s) { data_ += s; }
// joined 返回新对象,不修改自身 ------ 容易忘记接返回值
[[nodiscard]] StringBuffer joined(const std::string& s) const {
return StringBuffer(data_ + s);
}
[[nodiscard]] const std::string& str() const { return data_; }
};
// [[nodiscard]] 也可以标记整个类型: 该类型做返回值时一律不能忽略
struct [[nodiscard]] ImportantResult {
int code;
const char* detail;
};
ImportantResult compute() {
return {42, "success"};
}
// ============================================================
// C++17: [[maybe_unused]] --- 有意不用,别报警
// ============================================================
// 对变量、参数、函数、类型、非静态成员等都适用。
// 典型场景: assert 调试变量、接口要求的参数但实现中用不到、
// 模板特化中不用的参数。
// 场景 1: assert 用的 debug 变量
void debug_only_work() {
[[maybe_unused]] bool debug_flag = false; // 只在 assert 里用
// assert(debug_flag = check_invariants()); // release 下 assert 消失
}
// 场景 2: 接口要求的参数,但这个实现不需要
class NetworkHandler {
public:
virtual void on_data([[maybe_unused]] const char* data,
[[maybe_unused]] int length) {
// 基类默认实现什么都不做,但保留参数签名
}
};
// 场景 3: 模板中某些特化用不到参数
template <typename T>
void log_value([[maybe_unused]] const T& val) {
// 特化可能用到 val,但主模板不用
}
// 场景 4: 未使用的类型定义
class [[maybe_unused]] PreparedHeader {
// 这个类暂时定义了但还没用上,以后会用到
};
// ============================================================
// C++17: 未知属性被忽略 (语言保证)
// ============================================================
// C++17 明确规定: 编译器不认识 [[xxx]] 时必须忽略,不能报错。
// 这意味着你可以用 vendor-specific 属性,跨编译器不会崩。
// 例如 GCC 的 hot/cold 提示、MSVC 的 dllimport --- 其他编译器看到
// 虽然不认识但直接跳过,代码仍然可以编译。
#if defined(__GNUC__)
# define HOT_FUNC [[gnu::hot]] // GCC: 标记频繁调用的函数
# define COLD_FUNC [[gnu::cold]] // GCC: 标记极少调用的函数
#else
# define HOT_FUNC
# define COLD_FUNC
#endif
HOT_FUNC
int frequently_called() { return 42; }
COLD_FUNC
void rarely_called() { /* error recovery path */ }
// ============================================================
// 属性对比表速查
// ============================================================
// 标准 | 属性 | 作用
// ---------|--------------------------|---------------------------
// C++11 | [[noreturn]] | 函数不返回
// C++14 | [[deprecated]] | 标记废弃
// C++14 | [[deprecated("msg")]] | 带废弃原因
// C++17 | [[fallthrough]] | 有意不写 break
// C++17 | [[nodiscard]] | 返回值不能忽略
// C++17 | [[maybe_unused]] | 抑制未使用警告
// ============================================================
// main
// ============================================================
int main() {
std::cout << "======== C++11: [[noreturn]] ========\n";
std::cout << "fatal_error 被标记为 [[noreturn]],调用后不会返回。\n";
// fatal_error("test"); // 取消注释会 abort,先注释掉
std::cout << "\n======== C++14: [[deprecated]] ========\n";
// 编译时产生警告: 'old_api' is deprecated: Use new_api_v2() instead
old_api(); // ← 编译器警告!
new_api_v2(); // ← 无警告
LegacyConfig legacy; // ← 编译器警告!
// BetterConfig better; // ← 无警告 (未使用,被注释)
std::cout << "\n======== C++17: [[fallthrough]] ========\n";
std::cout << "--- Error level ---\n";
process_log(LogLevel::Error);
std::cout << "--- Warn level ---\n";
process_log(LogLevel::Warn);
std::cout << "--- Info level ---\n";
process_log(LogLevel::Info);
std::cout << "--- Debug level ---\n";
process_log(LogLevel::Debug);
std::cout << "\n======== C++17: [[nodiscard]] ========\n";
// 不检查返回值 --- 编译器警告 (返回值非 void,标记了 [[nodiscard]])
// connect_to_db("localhost"); // ← 编译器警告!(取消注释体验)
// 正确用法: 检查返回值
DbError err = connect_to_db("localhost");
if (err != DbError::Ok)
std::cout << "DB connection failed: " << static_cast<int>(err) << '\n';
StringBuffer buf("hello");
buf.append(" world"); // void 返回,无警告
// buf.joined("!"); // ← 编译器警告!返回了新对象但丢了
auto new_buf = buf.joined("!"); // 正确: 接收返回值
std::cout << "joined result: " << new_buf.str() << '\n';
// compute(); // ← 编译器警告!ImportantResult 被标记 [[nodiscard]]
auto result = compute(); // 正确: 接收
std::cout << "compute: " << result.code << ", " << result.detail << '\n';
std::cout << "\n======== C++17: [[maybe_unused]] ========\n";
debug_only_work(); // debug_flag 不会被警告
std::cout << "maybe_unused: 所有有意不用的变量/参数都不会产生警告\n";
std::cout << "\n======== Vendor-specific 属性 ========\n";
std::cout << "HOT_FUNC called: " << frequently_called() << '\n';
rarely_called();
return 0;
}
求值顺序规则
C++17之前:不确定性主导的蛮荒时代
在C++11/14中,表达式的求值顺序遵循"序列点"(sequenced before)概念,但标准故意留下了大量未指定(unspecified) 区域,留给编译器充分的优化空间。这导致两个常见问题:
-
跨平台的不可移植行为:同一段代码在不同编译器或优化级别下可能产生不同结果。
-
隐藏的未定义行为:若两个对同一标量对象(scalar object)的副作用无顺序(unsequenced),或一个副作用与一个值计算无顺序,程序即进入UB。
典型未指定/无顺序场景包括:
-
函数实参的求值顺序(未指定,可能交错)
-
大多数二元运算符的左右操作数求值顺序(如
+,-,*,/,%等完全未指定) -
赋值运算符左右操作数的求值顺序(未指定)
-
移位运算符左右操作数的求值顺序(未指定)
-
后缀表达式(如
a.b,a->b,a(b,c),new T(...)等)的子表达式顺序未指定
这些未指定性在单一表达式中修改同一变量时极易引爆UB。
C++17:引入刚性顺序,变未定义为可预测
C++17标准在 [intro.execution] 中大幅强化了求值顺序保证,核心原则是:表达式的结构应天然暗示子表达式的求值先后。具体新增保障如下:
| 运算符/表达式 | C++14 | C++17 |
|---|---|---|
a.b (成员访问) |
a与b相对顺序未指定 |
a先于b求值 |
a->b |
同上 | a先于b求值 |
a->*b |
同上 | a先于b求值 |
a[b] (下标) |
a与b相对顺序未指定 |
a先于b求值 |
a(b1, b2, ...) (函数调用) |
函数表达式与参数间未指定 | 函数表达式先于所有参数求值;参数间仍无顺序 |
a << b (移位) |
左右操作数顺序未指定 | 左操作数先于右操作数求值 |
a >> b |
同上 | 同上 |
a = b (赋值) |
左右操作数顺序未指定 | 右操作数先于左操作数求值(对内置运算符) |
复合赋值 a += b 等 |
同上 | 同上 |
new T(args...) |
分配内存与参数求值顺序未指定 | 分配内存先于参数求值(但参数间仍无序) |
重要说明 :上述"先于"保证同时包含值计算 (value computation)和副作用(side effect),意味着被先求值的表达式完全在另一表达式开始前完成。
这些变化消除了许多经典UB案例,并让代码行为跨平台一致。
表格总结
| 表达式 | C++14 顺序规则 | C++17 顺序规则 | 备注 |
|---|---|---|---|
a.b |
未指定 | a 先于 b |
成员访问 |
a->b |
未指定 | a 先于 b |
指针成员访问 |
a->*b |
未指定 | a 先于 b |
指向成员指针 |
a[b] |
未指定 | a 先于 b |
下标 |
f(args...) |
函数表达式与参数顺序未指定 | 函数表达式先于参数;参数间无序 | 函数调用 |
a << b |
未指定 | a 先于 b |
移位 |
a >> b |
未指定 | a 先于 b |
移位 |
a = b |
未指定 | b 先于 a |
内置赋值 |
a += b 等 |
未指定 | b 先于 a |
复合赋值 |
new T(args...) |
分配内存与参数无序 | 分配内存先于参数求值 | new 表达式 |
代码示例
cpp
#include <iostream>
#include <string>
#include <map>
// ============================================================
// C++17 求值顺序保证 --- 从"未定义行为陷阱"到"确定行为"
//
// C++17 之前的核心问题: 同一个表达式中的子表达式求值顺序
// 由编译器随意决定,没有任何约束。这意味着有副作用的子表达
// 式交错执行时可能产生未定义行为 (UB)。
//
// C++17 明确了以下规则:
// 1. a.b --- a 先于 b 求值
// 2. a->b --- a 先于 b 求值
// 3. a[b] --- a 先于 b 求值
// 4. a << b --- a 先于 b 求值
// 5. a >> b --- a 先于 b 求值
// 6. f(args) --- f (函数名字/可调用对象) 先于参数求值
// 7. a = b --- b (右值) 先于 a (左值) 求值
// ============================================================
int counter = 0;
// 带有副作用的辅助函数: 修改全局 counter 并打印
int next(const char* label) {
++counter;
int val = counter;
std::cout << " -> next(\"" << label << "\") = " << val << '\n';
return val;
}
void reset() { counter = 0; }
// ============================================================
// 1. operator<< 链式求值: i++ << i++
// ============================================================
void demo_shift_chain() {
std::cout << "1. 流插入链: cout << next(\"A\") << next(\"B\") << next(\"C\")\n";
// --- 假设使用 C++14 编译本文件 (-std=c++14) ---
// 在 C++17 之前, cout << f() << g() << h() 的求值顺序未指定。
// 如果 f/g/h 都有副作用且顺序敏感,结果完全不可预测。
// 在 GCC 中通常从右向左求值 (h→g→f),MSVC 可能从左向右。
// 编译器甚至可以在同一次编译中顺序不一致。
//
// --- 使用 C++17 (-std=c++17) ---
// C++17 明确: 左操作数先于右操作数求值。因此:
// next("A") → 输出 1
// next("B") → 输出 2
// next("C") → 输出 3
// counter 最终 = 3, 打印顺序也是 1 2 3
// 即从左到右严格顺序,没有歧义。
reset();
std::cout << " C++17 下必定输出 1 2 3:\n";
std::cout << " " << next("A") << " " << next("B") << " " << next("C") << '\n';
std::cout << " counter 最终 = " << counter << " (必定 = 3)\n";
}
// ============================================================
// 2. 下标运算符 a[b]: a 先于 b 求值
// ============================================================
// 重载 operator[] 以观察求值顺序
struct Array {
static int values[10];
Array() { std::cout << " -> Array 被访问\n"; }
int& operator[](int idx) {
std::cout << " -> operator[" << idx << "] 调用\n";
return values[idx];
}
};
int Array::values[10] = {0, 10, 20, 30, 40, 50, 60, 70, 80, 90};
Array get_array() {
std::cout << " -> get_array() 被调用\n";
static Array a;
return a;
}
void demo_subscript() {
std::cout << "\n2. 下标运算符: get_array()[next(\"idx\")] 中的顺序\n";
// C++17: a[b] 中 a 先于 b 求值。
// 在 get_array()[next("idx")] 中:
// - 先取 get_array() (拿到 Array 对象)
// - 再求值 next("idx") (得到索引)
// - 最后调用 operator[]
// 顺序确定,不会出现"先算索引,后算数组"的混乱。
reset();
int val = get_array()[next("idx")];
std::cout << " val = " << val << '\n';
}
// ============================================================
// 3. 赋值表达式: a = b 中 b 先于 a 求值
// ============================================================
void demo_assignment() {
std::cout << "\n3. 赋值表达式: v[i] = i++\n";
// 经典 UB 场景:
// int i = 0;
// v[i] = i++; // C++17 之前: UB (i 被读取和修改,无顺序保证)
//
// C++17: 右操作数 (i++) 先于左操作数 (v[i]) 求值。
// 关键: i++ 执行后 i 已经变成了 1,再求左操作数 v[i]
// 时使用的 i 已经是修改后的值。所以:
//
// 1. 计算 i++ → 返回旧值 0, 副作用: i = 1
// 2. 计算 v[i] → v[1] (因为 i 已经是 1 了!)
// 3. 赋值为 0 → v[1] = 0
//
// 结果: v[1] = 0, 而 v[0] 保持 -1 不变!
//
// 技巧: 不要写这种代码。虽然 C++17 定义了行为,但可读性极差。
// 分两行写: v[i] = i; ++i;
int data[5] = {-1, -1, -1, -1, -1};
int i = 0;
data[i] = i++; // C++17: 确定行为 (v[1]=0); C++14: UB
std::cout << " data[0] = " << data[0] << " (未变, 因为赋值目标是 data[1])\n";
std::cout << " data[1] = " << data[1] << " (被赋值为 i++ 的返回值 0)\n";
std::cout << " i = " << i << " (++ 后的值)\n";
}
// ============================================================
// 4. 成员访问: a->b 中 a 先于 b 求值
// ============================================================
struct Node {
int value;
Node* next_node = nullptr;
};
Node nodes[3] = {{1}, {2}, {3}};
// 获取节点指针 --- 带副作用
Node* get_node(int idx, const char* name) {
std::cout << " -> get_node(\"" << name << "\") = nodes[" << idx << "]\n";
return &nodes[idx];
}
void demo_member_access() {
std::cout << "\n4. 成员访问: get_node(...)->value 中的顺序\n";
// C++17: a->b 中 a 先于 b 求值
// 这对于重载的 operator-> 尤其重要: 必须保证先拿到智能指针所指对象,
// 再访问其成员。智能指针 (shared_ptr / unique_ptr) 就依赖此保证。
// 故意写一个看起来有歧义的表达式:
// get_node(A)->value + get_node(B)->value
// C++17: 每个 -> 内部先左后右,但两个 get_node 之间仍可能交错
reset();
int sum = get_node(next("A"), "first")->value
+ get_node(next("B"), "second")->value;
std::cout << " sum = " << sum
<< " (= nodes[" << (sum - 1) / 2 << "].value 相关)\n";
std::cout << " (两个 get_node 之间求值顺序仍不保证)\n";
}
// ============================================================
// 5. new 表达式中分配先于构造函数参数
// ============================================================
struct Tracker {
int id;
Tracker(int i) : id(i) {
std::cout << " -> Tracker(" << i << ") 构造完毕\n";
}
};
void demo_new_expression() {
std::cout << "\n5. new Tracker(next(\"arg\")) 分配内存先于参数求值\n";
// C++17: new-expression 中,内存分配先于所有参数求值。
// 这避免了 C++17 之前的一个问题: 如果在参数求值过程中
// 抛出异常,内存可能已经分配但无法释放。
reset();
Tracker* p = new Tracker(next("arg"));
std::cout << " p->id = " << p->id << '\n';
delete p;
}
// ============================================================
// 6. 函数参数之间 --- 仍然不保证顺序! (最常见的误解)
// ============================================================
int factory_a() { std::cout << " -> factory_a()\n"; return 1; }
int factory_b() { std::cout << " -> factory_b()\n"; return 2; }
void consume(int a, int b) {
std::cout << " consume(" << a << ", " << b << ")\n";
}
void demo_function_args() {
std::cout << "\n6. f(g(), h()): g 和 h 之间求值顺序仍不保证!\n";
std::cout << " (这是 C++17 的已知限制 --- 函数参数之间仍然是未指定的)\n";
// C++17 保证 f 本身先于参数求值,但 g() 和 h() 之间的顺序
// 仍然是 implementation-defined。不同编译器输出可能不同。
consume(factory_a(), factory_b());
std::cout << " 编译器可能先执行 factory_a() 或 factory_b(),"
"不能依赖任何特定顺序。\n";
}
// ============================================================
// 7. 花括号初始化列表: 从左到右 (C++11 起就保证)
// ============================================================
void demo_braced_init() {
std::cout << "\n7. 花括号初始化列表 { ... } --- 从 C++11 起就保序\n";
std::cout << " (非 C++17 新增 --- 只是列出做完整对比)\n";
reset();
int values[] = {next("A"), next("B"), next("C")};
std::cout << " values = {" << values[0] << ", " << values[1] << ", "
<< values[2] << "} (从左到右, C++11 起就保证)\n";
}
// ============================================================
// 8. 综合对比: 同一个表达式在 C++14 vs C++17 的表现
// ============================================================
void demo_summary_table() {
std::cout << "\n======== C++14 vs C++17 求值顺序对比 ========\n\n";
std::cout << " 表达式形式 C++14 C++17\n";
std::cout << " ─────────────────────────────────────────\n";
std::cout << " cout << f() << g() 未指定/可疑 f → g (保证)\n";
std::cout << " a->b 未指定 a → b (保证)\n";
std::cout << " a[b] 未指定 a → b (保证)\n";
std::cout << " a.b 未指定 a → b (保证)\n";
std::cout << " a = b 未指定 b → a (保证)\n";
std::cout << " new T(args) 未指定 分配→args (保证)\n";
std::cout << " f(g(), h()) g,h可交错 g,h可交错 (未变)\n";
std::cout << " {a, b, c} a→b→c a→b→c (C++11起)\n";
}
// ============================================================
// main
// ============================================================
int main() {
demo_shift_chain();
demo_subscript();
demo_assignment();
demo_member_access();
demo_new_expression();
demo_function_args();
demo_braced_init();
demo_summary_table();
return 0;
}
本期内容到这里就结束了,喜欢请点个赞谢谢
封面图自取:
