第25.4章:带棱角的声明

English

1. explicit(bool):有条件地显式

构造函数要么是 explicit,要么不是,通常对类型做一次选择。C++20 允许这个选择依赖 编译期条件:

#include <type_traits>

template <typename T>
struct box
{
    T value;

    explicit(std::is_integral_v<T>) box(T v) : value{v} {}
};

explicit(...) 里的表达式必须是 bool 类型的常量表达式。为 true 时构造函数是显式的;为 false 时是转换构造函数。常见用法是包装器 对某些元素类型接受隐式转换,对另一些不接受:

box<double> d = 1.0;   // OK: explicit(false)
box<int> i = 1;        // error: explicit(true)
box<int> j{1};         // OK: direct initialization

这不是运行时检查。条件在模板实例化期间求值,得到的构造函数要么显式,要么不是。它适合泛型包装器、 强类型 typedef 和单位类型,这些地方的转换策略取决于负载类型。

不要用 explicit(bool) 编码运行时策略。如果决定取决于值,那就不是转换规则;写一个 具名工厂函数。

2. [[no_unique_address]]:空成员

空类的大小至少是一字节,因为每个对象需要不同地址。空类型成员通常会占用这一字节加上填充:

struct empty {};

struct without
{
    empty e;
    int x;
};

struct with
{
    [[no_unique_address]] empty e;
    int x;
};

[[no_unique_address]] 告诉编译器这个成员不需要唯一地址,因此可以与其他成员重叠。 在本书使用的实现上,sizeof(without) 通常是 8,sizeof(with) 通常是 4。 标准允许重叠;它不要求特定大小。

static_assert(sizeof(with) <= sizeof(without));

这就是无状态分配器、比较器或删除器能存进容器而不让容器变大的方式。这个属性是布局许可,不是保证: 编译器需要时仍然可以给成员地址,而且布局可能因 ABI 而异。

不要把它用于地址身份重要的成员。如果代码取 &obj.e 并与其他对象的地址比较, 重叠存储会破坏假设。用它来存真正无状态的策略对象。

3. alignas 与 alignof

alignof(T) 询问类型的对齐要求。alignas(N) 为对象或类型请求更严格的对齐:

#include <cstddef>

struct alignas(64) cache_line
{
    std::byte data[64];
};

static_assert(alignof(cache_line) == 64);
static_assert(sizeof(cache_line) == 64);

alignas 可以提高对齐;不能低于类型的自然要求。带 alignas(64) 的类型 在任何地方都是 64 字节对齐,编译器会在数组和包含它的结构体里插入填充来保持这一点。

对齐不是大小。64 字节对齐的对象可以小于或大于 64 字节;对齐只固定起始地址。给每个对象过度对齐会浪费内存; 把它用于缓存行隔离、SIMD、内存映射 I/O 或文档化的 ABI,不要当通用性能提示。

类型的对齐也是共用体对齐取最严格成员对齐的原因,也是结构体成员顺序会改变大小的原因。布局是 ABI 的 一等公民。

4. offsetof

offsetof(type, member) 给出成员距标准布局对象起点的字节偏移:

#include <cstddef>

struct record
{
    int id;
    double value;
};

static_assert(offsetof(record, id) == 0);
static_assert(offsetof(record, value) >= sizeof(int));

offsetof 只对标准布局类型有定义。带虚函数、虚基类、非标准布局排列的私有数据成员, 或有多个访问区的类型,可能不能用。它是编译期宏,不是运行时反射调用;它不知道具体对象。

C++26 反射提供 std::meta::offset_of(第24.3章), 对反射问同样的问题。对于固定的标准布局结构体,宏仍然是可移植答案。

偏移依赖 ABI。对偏移做静态断言记录了你期望的布局;它不会让布局可移植。如果偏移必须匹配线格式, 用显式宽度和端序的序列化层,不要用裸结构体。

5. volatile 不是同步

volatile 告诉编译器对对象的每次访问都是可观察的,不能被优化掉,也不能相对其他 volatile 访问重排。它不提供原子性、互斥或线程间顺序。

volatile std::uint32_t *mmio = reinterpret_cast<volatile std::uint32_t *>(0x40000000u);
*mmio = 1;

这是合法用途:内存映射 I/O、硬件寄存器,或由信号处理函数修改的变量。这些情况下编译器必须真正执行 加载或存储。

这是不合法用途:

volatile int counter{};
++counter; // still a data race if another thread does the same

volatile 不会让 ++ 原子。两个线程仍然可以读到同一个值、加一、写回。 线程间通信用 std::atomic<int>;受保护状态超过一个字时用 mutex。 volatile 是给硬件和信号处理函数的,不是给调度器的。

6. 还有几个棱角

声明 它实际意味着什么
constexpr 成员函数 可以在常量表达式中调用;它不会让对象不可变
mutable 成员 在 const 成员函数里也可修改;用于缓存和 mutex
static 成员函数 没有 this;没有对象时不能访问非静态成员
explicit 转换运算符 需要转换或上下文转换;它阻止隐式转换
[[nodiscard]] 忽略返回值时警告;它不改变类型
[[fallthrough]] 记录有意的 switch 贯穿;它不改变控制流

共同点是:声明可以携带关于初始化、布局、链接、转换或诊断的规则。读规则,不要读关键字的声誉。 constexpr 函数不会自动更快;inline 变量不会自动内联; volatile 不是原子。

要点

细则章到这里结束。这些特性不是新的语言方向;它们是语言里早就存在、但很少单独成页的规则。