跳转至

嵌入式现代C++开发——属性(Attributes)

引言

你在写嵌入式代码的时候,有没有被这些情况搞得头大?

// 调用了一个返回错误码的函数,但忘了检查
initialize_hardware();  // 返回ErrorCode,但被忽略了

// 为了消除"未使用参数"警告,只能用注释或者奇怪的变量名
void handler(int event [[maybe_unused]]) {  // GCC扩展
    // ...
}

// 想标记某个函数已经过时,只能用编译器特定的宏
DEPRECATED("Use new_handler() instead")
void old_handler();

这些代码要么不标准,要么依赖编译器特定的扩展,跨平台移植的时候会炸。C++11引入的标准化属性语法[[attribute]]就是为了解决这个问题——让我们用一种标准的方式给编译器"提示"各种信息。

一句话总结:属性是给编译器的声明性提示,不改变程序的语义,但能帮助编译器发现错误或生成更好的代码。

但在嵌入式开发中使用属性需要理解各种属性的具体含义,因为:

  1. 某些属性(如[[nodiscard]])可以强制检查返回值,避免遗漏错误处理
  2. 某些属性(如[[likely]])可以提示分支预测,提升关键路径性能
  3. 某些属性(如[[no_unique_address]])可以优化内存布局,节省宝贵的RAM

我们把这些属性一个一个地拆开来看。


属性的基本语法

标准属性的形式

C++标准属性使用双中括号语法:[[attr]]。多个属性可以写在一起:[[attr1, attr2]],也可以分开写:[[attr1]] [[attr2]]

// 单个属性
[[nodiscard]] int check_status();

// 多个属性
[[nodiscard, deprecated("Use new_version()")]]
int old_function();

// 分开写(等效)
[[nodiscard]] [[deprecated("Use new_version()")]]
int old_function();

与编译器特定属性的对比

在标准化属性之前,各个编译器都有自己的语法:

// GCC/Clang的__attribute__
__attribute__((warn_unused_result)) int check_status();
__attribute__((unused)) void handler(int param);

// MSVC的__declspec
__declspec(deprecated("Use new_version()")) void old_function();

// C++标准属性(可移植)
[[nodiscard]] int check_status();
[[maybe_unused]] void handler(int param);
[[deprecated("Use new_version()")]] void old_function();

标准属性的优势是可移植——所有符合标准的编译器都必须支持。

属性的位置和作用域

属性可以放在很多位置,具体取决于属性的类型:

// 函数属性
[[nodiscard]] int func();

// 变量属性
[[maybe_unused]] int x;

// 类属性
[[nodiscard]] class MyClass { };

// 枚举属性
enum class [[deprecated]] OldEnum { };

// 语句属性(如fallthrough)
switch (value) {
    case 1:
        do_something();
        [[fallthrough]];  // 注意这里需要分号
    case 2:
        do_more();
}

属性的放置位置很重要,放错位置编译器会报错或者被忽略。


[[nodiscard]]:不能忽略的返回值

这个属性对于嵌入式开发来说可能是最重要的,它强制调用者检查函数的返回值。

基本用法

[[nodiscard]] ErrorCode initialize_hardware() {
    if (!check_power_supply()) {
        return ErrorCode::PowerFailure;
    }
    if (!setup_clocks()) {
        return ErrorCode::ClockError;
    }
    return ErrorCode::Ok;
}

// 现在,不检查返回值会产生编译警告或错误
initialize_hardware();  // 警告:忽略返回值

// 正确用法
if (initialize_hardware() != ErrorCode::Ok) {
    handle_error();
}

在嵌入式系统中,硬件初始化、传感器读取、通信操作等都有可能失败,忽略这些返回值会导致系统进入不确定状态。

C++20的自定义消息

C++20允许我们为[[nodiscard]]添加自定义消息:

[[nodiscard("Must check return value: hardware initialization may fail")]]
ErrorCode initialize_hardware() {
    // ...
}

// 如果调用者忽略返回值,编译器会显示自定义消息
initialize_hardware();
// 编译器输出:warning: ignoring return value of 'ErrorCode initialize_hardware()',
//            declared with attribute 'nodiscard': Must check return value:
//            hardware initialization may fail

应用在类和枚举上

[[nodiscard]]也可以应用到类或枚举上,这样所有返回该类型的函数都会自动检查返回值:

// 定义错误类型为nodiscard
[[nodiscard]] enum class ErrorCode {
    Ok,
    InvalidParam,
    Timeout,
    HardwareError
};

// 现在所有返回ErrorCode的函数都自动检查返回值
ErrorCode read_sensor(uint8_t id) {
    if (id > MAX_SENSORS) {
        return ErrorCode::InvalidParam;  // 调用者必须检查
    }
    // ...
}

HAL库封装中的应用

在封装HAL库时,[[nodiscard]]特别有用:

class UARTDriver {
public:
    // 发送数据,必须检查是否成功
    [[nodiscard]] bool send(const uint8_t* data, size_t length) {
        if (length > buffer_size_) {
            return false;  // 调用者必须处理
        }
        // ... 发送逻辑
        return true;
    }

    // 接收数据,返回实际接收的字节数(-1表示错误)
    [[nodiscard]] int receive(uint8_t* buffer, size_t max_length) {
        if (!rx_ready_) {
            return -1;  // 调用者必须检查
        }
        // ... 接收逻辑
        return actual_length;
    }
};

// 使用
UARTDriver uart;
if (!uart.send(data, len)) {
    handle_send_error();
}

int received = uart.receive(buffer, sizeof(buffer));
if (received < 0) {
    handle_receive_error();
}

[[maybe_unused]]:消除"未使用"警告

这个属性告诉编译器某个变量或参数可能不被使用,不要发出警告。

基本用法

// 函数参数未使用(比如为了符合接口规范)
void interrupt_handler(int irq_number [[maybe_unused]]) {
    // 某些中断处理函数可能不需要使用irq_number参数
    // 但为了符合中断向量表的接口规范,必须保留这个参数
    clear_interrupt_flag();
    // ...
}

// 变量未使用(条件编译场景)
#ifdef ENABLE_DEBUG
constexpr bool debug_mode = true;
#else
constexpr bool debug_mode [[maybe_unused]] = false;
#endif

与传统方法的对比

在标准属性之前,我们有几种方法处理未使用的警告:

// 方法1:使用(void)语句(老式做法)
void handler(int param) {
    (void)param;  // 告诉编译器"我故意不用的"
    // ...
}

// 方法2:使用编译器特定属性
void handler(int __attribute__((unused)) param) {
    // ...
}

// 方法3:使用注释
void handler(int /*param*/) {
    // ...
}

// 方法4:使用C++标准属性(推荐)
void handler(int [[maybe_unused]] param) {
    // ...
}

[[maybe_unused]]的优势是标准、清晰、可移植。

嵌入式场景应用

在条件编译和接口适配场景中,[[maybe_unused]]特别有用:

// 条件编译下的参数
void sensor_task(void* param [[maybe_unused]]) {
#ifdef USE_RTOS
    // 在RTOS环境下,param是任务参数
    TaskConfig* config = static_cast<TaskConfig*>(param);
    configure_sensor(config->port);
#else
    // 在裸机环境下,param不被使用
    configure_sensor(DEFAULT_PORT);
#endif
}

// 实现接口但某些参数不需要
class ISensor {
public:
    virtual void initialize(uint8_t address, [[maybe_unused]] int flags) = 0;
};

class SimpleSensor : public ISensor {
public:
    void initialize(uint8_t address, [[maybe_unused]] int flags) override {
        // SimpleSensor不支持flags参数,所以不使用
        setup_i2c(address);
    }
};

结构化绑定中的应用

在使用结构化绑定时,某些成员可能不被使用:

std::map<int, std::string> sensor_map = {{1, "Temp"}, {2, "Humidity"}};

for (const auto& [[maybe_unused], name] : sensor_map) {
    // 我们只需要name,不需要key
    printf("Sensor: %s\n", name.c_str());
}

不过这种写法可能会降低代码可读性,更好的做法可能是使用_作为占位符(C++20引入):

// C++20可以使用下划线作为占位符
for (const auto& [_, name] : sensor_map) {
    printf("Sensor: %s\n", name.c_str());
}

[[deprecated]]:标记过时代码

这个属性用于标记即将废弃的函数、类或变量,调用它们会产生编译警告。

基本用法

[[deprecated]] void old_function() {
    // 新代码应该用new_function()代替
}

void new_function() {
    // ...
}

// 调用old_function会产生警告
old_function();  // 警告:'old_function'已弃用

C++14的自定义消息

[[deprecated("Use new_function() instead, old_function will be removed in v2.0")]]
void old_function() {
    // ...
}

// 编译器会显示自定义消息
old_function();
// 警告:'old_function'已弃用: Use new_function() instead,
//       old_function will be removed in v2.0

固件版本迁移中的应用

在嵌入式固件升级时,[[deprecated]]特别有用:

// HAL v1.0 API
[[deprecated("Use HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET) instead")]]
void GPIOA_SetPin5(uint8_t state) {
    if (state) {
        GPIOA->BSRR = GPIO_BSRR_BS5;
    } else {
        GPIOA->BSRR = GPIO_BSRR_BR5;
    }
}

// 新的统一API
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);

// 旧的代码仍然可以工作,但会收到警告
GPIOA_SetPin5(1);  // 警告提示使用新API

类和成员函数的废弃

class SensorManager {
public:
    // 整个类废弃
    [[deprecated("Use NewSensorManager instead")]]
    class OldInterface { };

    // 单个函数废弃
    [[deprecated("Use get_sensor_data() which returns more information")]]
    bool read_sensor(uint8_t id, uint16_t* value);

    // 新的API
    SensorData get_sensor_data(uint8_t id);
};

枚举值的废弃

enum class SensorType {
    Temperature,
    Humidity,
    [[deprecated("Use Pressure instead")]]
    Barometer,  // 旧的名称
    Pressure     // 新的名称
};

// 旧代码
SensorType s = SensorType::Barometer;  // 警告

// 新代码
SensorType s = SensorType::Pressure;

[[fallthrough]]:明确switch穿透

这个属性用于标记switch语句中的有意穿透(intentional fallthrough),避免编译器警告。

为什么需要这个属性

在switch语句中,如果没有break语句,执行会"穿透"到下一个case。这可能是故意的,也可能是bug。编译器会对可能是bug的情况发出警告。

// 可能是bug
switch (value) {
    case 1:
        do_something();
        // 忘了写break!
    case 2:
        do_more();
        break;
}

[[fallthrough]]告诉编译器"这个穿透是我故意的"。

基本用法

void handle_event(uint8_t event) {
    switch (event) {
        case 0x01:
            // 处理事件0x01
            toggle_led(LED1);
            [[fallthrough]];  // 明确表示有意穿透

        case 0x02:
            // 处理事件0x02(也会处理0x01的部分)
            toggle_led(LED2);
            break;

        case 0x03:
            // 只处理事件0x03
            toggle_led(LED3);
            break;

        default:
            handle_unknown_event(event);
            break;
    }
}

嵌入式状态机应用

在状态机实现中,[[fallthrough]]特别有用:

enum class State {
    Idle,
    Initializing,
    Running,
    Paused,
    Error
};

void StateMachine::handle_event(Event event) {
    switch (current_state_) {
        case State::Idle:
            if (event == Event::Start) {
                current_state_ = State::Initializing;
            }
            [[fallthrough]];  // Idle和Initializing共享初始化逻辑

        case State::Initializing:
            initialize_hardware();
            if (init_success()) {
                current_state_ = State::Running;
            } else {
                current_state_ = State::Error;
            }
            break;

        case State::Running:
            if (event == Event::Pause) {
                current_state_ = State::Paused;
            } else if (event == Event::Stop) {
                current_state_ = State::Idle;
            } else {
                run_normal_operation();
            }
            break;

        case State::Paused:
            if (event == Event::Resume) {
                current_state_ = State::Running;
            }
            break;

        case State::Error:
            if (event == Event::Reset) {
                current_state_ = State::Idle;
            }
            break;
    }
}

注意事项

[[fallthrough]]必须放在case语句的最后一条语句之后,下一个case标签之前,而且后面必须跟分号:

// ❌ 错误:fallthrough不是最后一条语句
switch (x) {
    case 1:
        do_something();
        [[fallthrough]];
        do_more();  // 这行代码永远不会执行!
    case 2:
        // ...
}

// ❌ 错误:fallthrough后面没有分号
switch (x) {
    case 1:
        do_something();
        [[fallthrough]]  // 缺少分号
    case 2:
        // ...
}

// ✅ 正确
switch (x) {
    case 1:
        do_something();
        [[fallthrough]];
    case 2:
        // ...
}

另外,最后一个case或default后面不需要[[fallthrough]],因为没有东西可以穿透了。


[[likely]]和[[unlikely]]:分支预测提示

C++20引入的这对属性用于告诉编译器某个分支更可能被执行,编译器可以据此优化代码布局。

基本概念

现代CPU有分支预测功能,但编译器也可以通过重新排列代码来提高指令缓存命中率。[[likely]][[unlikely]]是给编译器的提示。

// 告诉编译器这个条件更可能为真
if (error == ErrorCode::Ok) [[likely]] {
    // 正常路径
    process_data();
} else {
    // 错误路径
    handle_error();
}

嵌入式场景应用

在嵌入式系统中,某些分支明显比其他分支更频繁:

// 中断服务程序
void UART1_IRQHandler() {
    if (uart1_rx_ready) [[likely]] {
        // 大多数中断是接收数据中断
        process_rx_data();
    } else {
        // 少数是其他事件(错误、发送完成等)
        handle_other_event();
    }
}

// 错误检查
ErrorCode read_sensor(uint8_t id, uint16_t* value) {
    if (id < MAX_SENSORS) [[likely]] {
        *value = sensor_registers[id];
        return ErrorCode::Ok;
    } else [[unlikely]] {
        return ErrorCode::InvalidParam;
    }
}

性能影响

分支预测提示的性能影响取决于具体场景:

  1. 热点代码:在频繁执行的循环或中断处理中,提示可能带来明显改善
  2. 冷热路径明显:如果某个分支99%都会执行,提示编译器可以优化代码布局
  3. 现代CPU:现代CPU的动态分支预测已经很强大,静态提示可能帮助有限
// 高频循环中的分支预测
void process_buffer(const uint8_t* buffer, size_t length) {
    for (size_t i = 0; i < length; ++i) {
        if (buffer[i] != 0xFF) [[likely]] {
            // 大多数数据不是0xFF
            process_byte(buffer[i]);
        } else [[unlikely]] {
            // 0xFF是特殊标记,很少出现
            handle_escape_sequence();
        }
    }
}

建议在实际代码中进行性能测试,确定分支预测提示是否真的带来改善。


[[no_unique_address]]:空基类优化

C++20引入的[[no_unique_address]]属性用于优化包含空类的内存布局,在嵌入式系统中可以节省宝贵的RAM。

什么是空类

空类是指没有非静态数据成员的类:

struct Empty {
    void foo() {}
    int bar() { return 42; }
};

struct Empty2 {
    [[no_unique_address]] Empty e;  // e不占用空间
    int x;
};

static_assert(sizeof(Empty) == 1);  // 空类大小至少为1(C++标准要求)
static_assert(sizeof(Empty2) == sizeof(int));  // e不占用额外空间

空基类优化(EBO)

传统的EBO技巧是通过继承来避免空类占用空间:

// 传统方法:通过继承实现EBO
template<typename T>
class Container : private T {
    int data;
    // T不占用额外空间
};

// C++20方法:使用[[no_unique_address]]
template<typename T>
class Container {
    [[no_unique_address]] T allocator_;
    int data;
    // allocator_不占用额外空间(如果T是空类)
};

嵌入式场景应用

在嵌入式系统中,策略类通常只包含类型定义或静态函数,可以作为空类:

// 空的分配器策略
struct MallocAllocator {
    void* allocate(size_t size) {
        return malloc(size);
    }
    void deallocate(void* p) {
        free(p);
    }
};

// 空的锁策略(用于单线程环境)
struct NullMutex {
    void lock() {}
    void unlock() {}
};

// 使用[[no_unique_address]]的策略类
template<typename T, typename Allocator = MallocAllocator, typename Mutex = NullMutex>
class DataBuffer {
public:
    void push(const T& item) {
        mutex_.lock();
        // ... 添加数据
        mutex_.unlock();
    }

private:
    [[no_unique_address]] Allocator allocator_;
    [[no_unique_address]] Mutex mutex_;
    T* data_;
    size_t size_;
    size_t capacity_;
};

// 在单线程环境下,NullMutex不占用空间
DataBuffer<int, MallocAllocator, NullMutex> buffer1;
static_assert(sizeof(buffer1) == sizeof(void*) * 3 + sizeof(size_t) * 2);

// 在多线程环境下,使用真实的互斥锁
DataBuffer<int, MallocAllocator, std::mutex> buffer2;
// 这时mutex_会占用实际空间

注意事项

[[no_unique_address]]有一些需要注意的地方:

  1. 不同类型的空对象可能共享同一地址
struct A {
    [[no_unique_address]] Empty e1;
    [[no_unique_address]] Empty e2;
    int x;
};
// e1和e2可能有相同的地址!
  1. 取地址操作可能返回相同的值
A a;
bool same_address = (&a.e1 == &a.e2);  // 可能为true
  1. 主要用于空类优化:如果类不是空的,[[no_unique_address]]不起作用:
struct NonEmpty {
    int data;
};

struct Test {
    [[no_unique_address]] NonEmpty e;  // e仍然占用空间
    int x;
};
static_assert(sizeof(Test) == 2 * sizeof(int));  // e占用sizeof(int)

其他属性简介

[[noreturn]]:函数不返回

标记函数不会返回到调用者(要么正常终止,要么抛出异常):

[[noreturn]] void fatal_error(const char* msg) {
    printf("FATAL: %s\n", msg);
    while (1) {
        // 死循环
    }
}

void check_condition(bool condition) {
    if (!condition) {
        fatal_error("Condition failed!");  // 编译器知道这里不会返回
    }
    // 后续代码可以被优化(不需要考虑"返回"的情况)
}

[[carries_dependency]]:内存序依赖

用于lock-free编程,告诉编译器不要破坏内存依赖关系(高级话题,超出了本教程范围)。

C++20合约(Contracts)

C++20引入了合约属性[[expects]][[assert]][[ensures]],但截至2024年,主流编译器(GCC、Clang、MSVC)对合约的支持仍然有限或完全不支持。在嵌入式开发中,建议使用assert宏或者静态断言来替代。

// C++20合约(可能不被编译器支持)
int divide(int a, int b)
[[expects: b != 0]]
[[ensures: ret_value * b == a]]
{
    return a / b;
}

// 当前推荐的替代方案
int divide(int a, int b) {
    assert(b != 0 && "Divisor cannot be zero");
    int result = a / b;
    assert(result * b == a && "Division correctness check");
    return result;
}

嵌入式实战:完整的HAL封装示例

让我们把学到的属性应用到一个完整的HAL库封装中:

class UARTDriver {
public:
    // 错误类型——强制检查
    [[nodiscard]] enum class Error {
        Ok,
        InvalidParam,
        Busy,
        Timeout,
        HardwareError
    };

    // 初始化——必须检查返回值
    [[nodiscard("Must check initialization result")]]
    Error init(uint32_t baudrate);

    // 发送数据——返回是否成功
    [[nodiscard]]
    bool send(const uint8_t* data, size_t length);

    // 接收数据——返回实际接收长度或错误
    [[nodiscard]]
    int receive(uint8_t* buffer, size_t max_length);

    // 旧的API——标记为废弃
    [[deprecated("Use init() instead")]]
    void initialize(uint32_t baudrate);

    // 中断处理函数——参数可能不使用
    void irq_handler([[maybe_unused]] uint32_t irq_flags) {
#ifdef USE_DMA
        // 在DMA模式下,irq_flags有特定用途
        if (irq_flags & DMA_TC_FLAG) {
            handle_dma_complete();
        }
#else
        // 在中断模式下,irq_flags不使用
        handle_rx_interrupt();
#endif
    }

private:
    void handle_rx_interrupt() {
        if (rx_ready_) [[likely]] {
            // 大多数情况下,接收缓冲区是就绪的
            process_rx_data();
        } else [[unlikely]] {
            // 少数情况下,接收缓冲区未就绪(溢出等)
            handle_rx_error();
        }
    }

    // 空的锁策略(单线程模式)
    struct NullLock {
        void lock() {}
        void unlock() {}
    };

    [[no_unique_address]] NullLock lock_;
    volatile uint32_t* base_address_;
    bool rx_ready_;
};

// 使用示例
UARTDriver uart1;

// 初始化——必须检查返回值
if (uart1.init(115200) != UARTDriver::Error::Ok) {
    handle_init_error();
}

// 发送数据——检查是否成功
uint8_t data[] = {0x01, 0x02, 0x03};
if (!uart1.send(data, sizeof(data))) {
    handle_send_error();
}

// 接收数据
uint8_t rx_buffer[128];
int received = uart1.receive(rx_buffer, sizeof(rx_buffer));
if (received > 0) {
    process_data(rx_buffer, received);
}

常见的坑

坑1:nodiscard被显式绕过

[[nodiscard]]只是一个提示,可以被显式绕过:

[[nodiscard]] ErrorCode check_status();

// ❌ 显式忽略返回值
(void)check_status();  // 没有警告!

// 或者
static_cast<void>(check_status());  // 没有警告!

这其实是特性而不是bug——有些情况下我们确实需要忽略返回值。但团队代码规范中可能需要禁止这种做法。

坑2:属性位置错误

某些属性必须在特定位置才能生效:

// ❌ 错误:deprecated的位置不对
class [[deprecated]] MyClass {
    void old_method [[deprecated]];  // 这是错的!
};

// ✅ 正确
class [[deprecated("Use NewClass instead")]] MyClass { };
class MyClass {
    [[deprecated("Use new_method() instead")]]
    void old_method();
};

坑3:编译器支持差异

虽然属性是标准化的,但不同编译器的支持程度可能不同:

// C++20的属性可能不被老编译器支持
if (condition) [[likely]] {  // GCC 10+, Clang 12+才支持
    // ...
}

// 建议使用编译器宏进行条件编译
#if __cplusplus >= 202002L && \
    (defined(__GNUC__) && __GNUC__ >= 10 || \
     defined(__clang__) && __clang_major__ >= 12)
    #define LIKELY [[likely]]
    #define UNLIKELY [[unlikely]]
#else
    #define LIKELY
    #define UNLIKELY
#endif

if (error == ErrorCode::Ok) LIKELY {
    // ...
}

坑4:属性与宏定义的交互

在宏定义中使用属性需要小心:

// ❌ 可能不工作
#define DEPRECATED_FUNC(func) \
    [[deprecated]] func

DEPRECATED_FUNC(void old_func());  // 语法错误

// ✅ 正确做法
#define DEPRECATED_FUNC(msg) \
    [[deprecated(msg)]]

[[deprecated("Use new_func()")]] void old_func();

小结

C++属性是现代C++中强大的声明性工具:

常用属性总结

属性 引入版本 主要用途 嵌入式场景
[[nodiscard]] C++17 强制检查返回值 HAL API、错误处理
[[maybe_unused]] C++17 消除未使用警告 条件编译、接口适配
[[deprecated]] C++14 标记废弃API 固件迁移、版本管理
[[fallthrough]] C++17 标记有意穿透 状态机、协议解析
[[likely]]/[[unlikely]] C++20 分支预测提示 中断处理、热点循环
[[no_unique_address]] C++20 空基类优化 策略类、内存优化
[[noreturn]] C++11 标记不返回函数 致命错误处理

实践建议

  1. 优先使用场景
  2. HAL库API设计(大量使用[[nodiscard]]
  3. 固件版本迁移(使用[[deprecated]]
  4. 策略类设计(使用[[no_unique_address]]

  5. 谨慎使用场景

  6. [[likely]]/[[unlikely]]需要实测验证效果
  7. 某些老编译器可能不完全支持C++20属性

  8. 嵌入式特别关注

  9. [[nodiscard]]可以避免遗漏错误检查,提高系统可靠性
  10. [[no_unique_address]]可以节省RAM,在资源受限环境下很有用
  11. [[fallthrough]]可以让状态机代码更清晰

  12. 团队规范建议

  13. 建立团队属性使用规范
  14. 禁止显式绕过[[nodiscard]]
  15. 统一使用标准属性而非编译器特定扩展

属性是编译器帮我们发现问题的重要工具。在嵌入式开发中,合理使用属性可以让代码更安全、更高效,让编译器成为我们的"静态分析助手"。配合前面学过的auto、结构化绑定、范围for循环等特性,现代C++已经发展成一门既强大又安全的系统编程语言。

到这里,关于C++实用语言特性的介绍就告一段落了。下一章我们将深入探讨三路比较运算符(Spaceship Operator,C++20),看看这个来自Perl/Ruby的特性如何简化我们的比较逻辑。