这份有限范围的汇编收集了不需要单独文章的语言和工具链相关规则。每节都说明控制该功能的边界,因为看起来相似的 C++ 写法可能产生不同的所有权、查找或生命周期效果。
资源所有权和可选状态 #
所有权是负责在正确时间释放资源。使用 RAII 建模:在构造时获取,在析构时释放,并使对象不变量在异常控制流中保持成立。当 std::unique_ptr 或 std::vector 等成员已经实现资源策略时,优先使用 Rule of Zero。直接管理裸资源的类型必须定义连贯的析构、复制策略、移动策略和失败策略。
std::make_unique<T>(args...) 表达独占所有权,而不会暴露独立的 new;它从 C++14 起可用。std::make_shared<T>(args...) 可以合并控制块和对象分配。合并分配并不总是合适:weak_ptr 可以在对象销毁后保持该分配存活,自定义删除或已有裸指针也可能要求直接构造。需要直接构造时,应在同一个完整表达式中建立智能指针所有者。在 C++17 收紧函数参数求值顺序之前,如果在更大的调用中将分配和所有权建立分开,另一个参数抛出异常也可能导致泄漏。
根据所有权和身份选择可选子对象的表示:
- 具有有效空状态的直接成员没有间接层,但该状态必须明确无歧义。
std::optional<T>在包含对象中存储可能不存在的T;emplace开始被包含对象的生命周期,而无需单独分配。std::unique_ptr<T>表示对独立对象的排他所有权,并支持不完整类型或多态类型,但代价是间接层和可为空句柄。
optional 和堆分配都不能让对象超出其所有者生命周期。引用、指针或
std::reference_wrapper<T> 是非所有者句柄;被引用对象必须比每次使用存活更久。当面向值的接口(如容器或 std::bind)必须携带引用时,std::ref 和 std::cref 很有用。
接口、运算符和对象模型 #
对称二元运算符通常应是非成员函数,以便转换可以同时考虑两个操作数。将其声明为
friend 会授予范围明确的私有访问权限;友元关系不会使它成为成员。
class Quantity {
public:
explicit Quantity(int value) : value_{value} {}
friend Quantity operator+(Quantity left, const Quantity& right) {
left.value_ += right.value_;
return left;
}
private:
int value_;
};多态类具有虚函数。标准规定虚派发行为,但不规定 vptr、vtable、它们的布局或 ABI。许多实现使用隐藏指针和函数指针表,但多重继承和编译器选项会使基于该模型的假设失效。应用代码必须依赖虚函数,而不是表示细节。
抽象类在类定义完成后即完整,即使纯虚函数阻止直接实例化。不完整类仅被声明,不能用于 sizeof 或直接数据成员。在非虚菱形继承中,派生对象包含两个基类子对象,因此未限定的继承成员可能产生歧义。但静态数据成员属于声明它的类,不会为每个基类子对象复制一份。
struct A {
int value;
static int count;
};
struct B : A {};
struct C : A {};
struct D : B, C {};
int A::count = 0;
D object;
// object.value is ambiguous: object contains two A base subobjects.
D::count = 1; // One static member; no A object is selected.
表达式、引用和解析 #
每个非 void 表达式都是 prvalue、xvalue 或 lvalue。lvalue 和 xvalue 是 glvalue,因为它们标识实体;prvalue 和 xvalue 是 rvalue。命名变量(包括声明为 T&& 的变量)是 lvalue 表达式。标量 prvalue 对内置取址运算符没有可取地址的身份。
引用不是对象。命名引用是其被引用类型的 lvalue 表达式。模板替换可以形成引用的引用;
折叠会将 T& &、T& && 和 T&& & 变为 T&,只有 T&& && 保留为 T&&。这些规则支持转发,而不创建嵌套引用对象。
const 成员函数约束隐式对象参数。const 重载可以在 const 和非 const 对象上运行;非 const 重载不能在 const 对象上运行。在 const 成员中,不能通过 *this 修改非 mutable 成员。
当文本可以解析为声明时,C++ 会将其解析为声明。这种最令人烦恼的解析不会构造对象:
Widget widget(Factory()); // May declare a function.
Widget object{Factory{}}; // Unambiguously constructs an object.
整数转换和移位 #
当 int 可以表示所有值时,整数提升会将 bool、字符类型、short 以及其他秩低于
int 的符合条件类型转换为 int;否则提升为 unsigned int。通常算术转换随后选择公共类型。因此,在 int 能表示每个 unsigned char 值的常见系统中,对 unsigned char 值进行的算术会作为 int 算术进行。
unsigned char left = 42;
unsigned char right = 55;
int sum = left + right;移位时,两个操作数都会进行整数提升。负计数,或计数大于等于提升后左操作数宽度,都是未定义行为。可移植地进行位操作时,应使用无符号类型并验证计数。C++ 没有 Java 风格的 >>>;无符号值的右移会移入零位。C++20 将有符号右移规定为算术右移,但表示形式重要时,面向位的代码仍应避免有符号操作数。
可调用对象、查找和多态 #
每个 lambda 表达式都有独立的未命名闭包类型;decltype 可以命名特定闭包对象的类型。捕获内容由闭包对象存储,因此捕获的引用不会延长生命周期。泛型 lambda 具有模板化的调用运算符,并会针对使用的参数类型实例化行为。
未限定函数调用会结合普通查找和参数依赖查找(ADL)。ADL 会考虑与参数类型关联的命名空间和类,从而支持如下自定义模式:
using std::swap;
swap(left, right); // Standard fallback plus an associated-namespace overload.
限定调用会抑制 ADL。不要依赖基本类型的 ADL:int 不会将调用与无关命名空间关联。
声明为函数类型的函数参数会调整为指向函数类型的指针。在适用上下文中,函数名也会转换为函数指针。
void callback(void);
void invoke(void f(void));
void invoke_pointer(void (*f)(void));
void use(void) {
invoke(callback);
invoke_pointer(&callback);
}C++ 增加了重载集合、模板和成员函数指针,因此函数名可能需要目标类型,才能选择一个重载。
特设多态根据静态参数类型选择重载。参数多态根据编译时要求实例化模板。子类型多态在运行时通过基类指针或引用派发虚调用。类型擦除(例如 std::function、概念/模型包装器或基类接口)会在边界处用运行时接口替换具体类型。它可以减少头文件和 ABI 耦合,但可能增加间接层、分配、包装器存储和运行时检查。
延迟初始化和并发 #
函数局部静态变量的初始化是线程安全的:如果多个线程并发进入声明,初始化只完成一次后才会有线程继续;如果抛出异常,之后可以重试。对于在线程之间共享的延迟初始化成员,应使用 std::mutex、std::call_once 或适当的原子协议保护状态转换。
const Settings& settings() {
static const Settings value = load_settings();
return value;
}volatile 既不会使访问具有原子性,也不会建立 happens-before 关系。它不能替代互斥量、条件变量或 std::atomic,也不保证刷新缓存或访问主内存。仅当平台契约(例如内存映射 I/O)要求可观察的 volatile 访问时,才使用它。
翻译单元依赖 #
包含头文件会创建编译时依赖:当头文件中的声明变更需要客户端重新编译时,就存在这种依赖。在公共接口中暴露具体容器或其迭代器类型,会使客户端与该表示耦合。窄的抽象接口或 Pimpl 边界可以保留实现自由,但每种方式都会增加接口或间接成本。
在函数内部构造的自动对象每次调用都会构造和销毁一次。仅为避免构造而将它移出循环会改变其生命周期,并可能使状态、资源或同步超出预期作用域。
评论
在 GitHub 上阅读或评论(发表评论需要登录)