C++17新特性:__had_include/属性/求值顺序规则

本期我们接着来介绍C++17的新特性:

相关代码上传至gitee:楼田莉子/Linux学习 - Gitee.com

目录

__had_include

前言

拥有之后

[价值所在:__has_include 解决了什么问题](#价值所在:__has_include 解决了什么问题)

场景:在Windows上使用DirectSound,在Linux上使用ALSA

场景:实现"可选依赖"

进阶技巧与注意事项

属性

[C++11 属性:确立标准语法](#C++11 属性:确立标准语法)

[[noreturn]]

[[carries_dependency]]

[C++14 属性:实用化开端](#C++14 属性:实用化开端)

[[[deprecated]] 和 [[deprecated("reason")]]](#[[deprecated]] 和 [[deprecated("reason")]])

[C++17 属性:走向精细化控制](#C++17 属性:走向精细化控制)

[[fallthrough]]

[[nodiscard]]

[[maybe_unused]]

属性位置的扩展

属性列举

代码示例

求值顺序规则

C++17之前:不确定性主导的蛮荒时代

C++17:引入刚性顺序,变未定义为可预测

表格总结

代码示例


__had_include

前言

在C++17以前,__has_include并非标准C++的一部分,这给需要依赖特定库或平台特性的项目带来了巨大的兼容性难题。开发者通常采用两类方法来适应不同的编译环境:

  • 借助外部构建系统 :最正统的方法是使用autoconfCMake这类构建工具。它们在编译前运行测试程序,检测环境中的头文件、库和函数,并据此生成一个config.h头文件,其中定义了如#define HAVE_NVML_H 1的宏。开发者随后通过#ifdef HAVE_NVML_H来编写条件代码。

    • 缺点:这种方法使得构建流程变得复杂,对于一个快速迭代的小型项目而言,维护构建脚本的成本可能比项目本身还高。
  • 使用编译器的非标准扩展 :许多编译器预定义了平台相关的宏,例如_WIN32(Windows)、__linux__(Linux)等。开发者可以通过检查这些宏来判断当前编译环境。

    • 缺点:这是一种"间接推断",不仅代码中充斥着难以理解的宏判断,而且在适配新平台或编译器时,需要手动添加新的宏分支,维护成本高。

拥有之后

C++17将__has_include正式纳入标准,使其成为一个原生的预处理常量表达式。它像是一种"如果我包含这个头文件会怎样?"的合法"试探"。

其工作流非常直接:

  1. 在预处理阶段,编译器遇到__has_include(header)

  2. 编译器会在其标准包含路径中查找指定的头文件。

  3. 如果文件被成功定位,整个表达式被求值为1 (真);反之则为0 (假)。

重要的是,这个过程是零成本的------它只发生在编译期,既不会真的#include该文件,也不会在运行期产生任何代码

价值所在:__has_include 解决了什么问题

这个特性主要解决了以下三个痛点:

  • 标准化跨平台检查 :你不再需要依赖构建系统的探测或编译器专属宏。__has_include提供了一种统一的、原生的条件编译方式。只要代码是用C++17标准编译的,这个特性就能用。

  • 简化构建流程 :对于许多项目,你可以用一行__has_include来替代复杂的构建系统探测脚本,例如CMakecheck_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]]

语义 :标记函数不会正常返回(如通过 throwabort() 或无限循环终止)。
用途

  • 消除"控制流到达非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/elseswitch 语句的前置条件 if (x > 0) [[likely]] { ... }
[[unlikely]] C++20 提示编译器该分支不太可能被执行,辅助分支预测优化。 if/elseswitch 语句的前置条件 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) 区域,留给编译器充分的优化空间。这导致两个常见问题:

  1. 跨平台的不可移植行为:同一段代码在不同编译器或优化级别下可能产生不同结果。

  2. 隐藏的未定义行为:若两个对同一标量对象(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 (成员访问) ab相对顺序未指定 a先于b求值
a->b 同上 a先于b求值
a->*b 同上 a先于b求值
a[b] (下标) ab相对顺序未指定 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;
}

本期内容到这里就结束了,喜欢请点个赞谢谢

封面图自取:

相关推荐
程序员cxuan1 小时前
Codex 把我家烂网给优化后,我 TM 直接原地起飞了。
人工智能·后端·程序员
IT_陈寒1 小时前
Redis批量删除踩了坑,原来DEL命令不是万能的
前端·人工智能·后端
香蕉鼠片2 小时前
Python进阶学习
开发语言·python
摇滚侠2 小时前
Java 零基础全套教程,File 类与 IO 流,笔记 177-178
java·开发语言·笔记
叫我少年2 小时前
C# 命名空间与 using 指令 — 文件范围、全局导入、别名
后端
ytttr8732 小时前
OPC UA 协议栈 C 语言实现
c语言·开发语言·mfc
song5012 小时前
Ascend C 算子开发:从入门到上手
c语言·开发语言·图像处理·人工智能·分布式·flutter·交互
小a杰.2 小时前
Ascend C编程语言进阶:高性能算子开发技巧
android·c语言·开发语言
全糖可乐气泡水2 小时前
Codex适配国产信创环境安装部署与技术适配全解析
开发语言·git·python·算法·百度