实现惯用法应明确所有权、生命周期或依赖边界。只有在相关的失效和复杂性规则对读者仍然可见时,简洁语法才有价值。
序列容器的 Erase-Remove #
std::remove 和 std::remove_if 是算法;它们重新排列范围,使保留元素占据前缀,并返回新的逻辑末端。它们既不会擦除元素,也不会改变容器大小。当容器提供范围擦除重载时,应立即擦除后缀。
std::vector<int> values{1, 2, 2, 3, 4};
values.erase(std::remove(values.begin(), values.end(), 2), values.end());
values.erase(std::remove_if(values.begin(), values.end(), is_invalid), values.end());从 C++20 开始,std::erase(values, value) 和 std::erase_if(values, predicate) 为支持它们的标准容器表达完整操作。当项目的语言和库基线提供这些重载时,应优先使用它们。
移除算法可能移动或赋值保留元素,因此指向位置发生变化的值的引用和迭代器不再标识同一个值。后续 erase 遵循容器的失效规则。对于 std::vector,擦除位置及其之后的迭代器、指针和引用都会失效;代码必须重新获取它们。节点容器具有不同规则,因此不要将 vector 的假设转用于 std::list、std::map 或 std::unordered_map。
Pimpl 定义表示边界 #
指向实现的惯用法将私有表示从公共头文件中移出。客户端针对稳定接口编译,而不是针对每个私有成员类型编译,因此表示更改可以减少重新构建;当其余公共 ABI 也受到控制时,还可以保持 ABI 边界。
// widget.hpp
class Widget {
public:
Widget();
~Widget();
Widget(Widget&&) noexcept;
Widget& operator=(Widget&&) noexcept;
Widget(const Widget&);
Widget& operator=(const Widget&);
private:
class Implementation;
std::unique_ptr<Implementation> implementation_;
};
// widget.cpp
class Widget::Implementation {
// Private state and dependencies.
};
Widget::~Widget() = default;std::unique_ptr<Implementation> 允许在头文件中使用前置声明,但销毁需要在实例化删除器的位置使 Implementation 完整。在 Implementation 定义之后将析构函数定义移到类外,可以提供这种完整性。复制操作需要显式的深拷贝策略,因为 unique_ptr 不可复制;如果 Widget 有意设计为仅可移动,则省略复制操作。
Pimpl 具有成本:通常需要一次分配、指针间接访问、较差的局部性以及可能更复杂的调试。它也不会使每次实现更改都对 ABI 安全:内联函数、异常规范、虚函数、基类和公开的参数类型仍然是契约的一部分。
不使用 Pimpl 的依赖缩减 #
使用能保护预期契约的最轻边界:
- 当头文件只存储某个类的指针或引用时,使用前置声明。
- 当调用方需要行为而非表示时,依赖窄接口。
- 仅当编译时耦合可以接受时,才将模板定义和值成员保留在头文件中。
- 当具体私有布局、昂贵的依赖图或 ABI 承诺足以证明运行时间接访问合理时,使用 Pimpl。
类型别名泄露表示的程度可能与数据成员相同。返回 std::vector<T>::iterator 会将客户端绑定到 vector 的迭代器类型;之后更换容器会成为源代码变化,并可能成为 ABI 变化。
间接访问与值语义 #
直接值成员具有单一分配域、清晰的所有权和通常更好的缓存局部性。std::optional<T> 表示可能缺失的就地值,无需单独分配。std::unique_ptr<T> 表示对单独分配对象的独占所有权,适用于多态、不完整类型、大型可选状态或独立身份。
选择间接访问是因为对象需要其语义属性,而不只是因为成员很大。间接访问会使可空性、分配失败、指针生命周期和额外的别名层级成为实现的一部分。值语义则使复制和移动成为类契约的一部分;当调用方需要独立值时,应选择值语义。
评论
在 GitHub 上阅读或评论(发表评论需要登录)