C++ 设计指南

最后更新: 2023-02-05 10:36:28 +0800 +0800
原文以英文撰写。您正在阅读AI翻译版本。

C++ 接口应在类型和签名中编码所有权、生命周期和可替代性。C++ Core Guidelines 的 Rule of Zero 正是这一设计的结果:由 RAII 成员组成的领域值不应手动重复它们的复制、 移动和销毁行为。

资源所有者和领域值 #

资源所有者类型获取一个资源,在析构函数中释放它,并说明是否允许复制或转移。句柄表示 对资源的访问;所有者句柄还承担释放资源的责任。当标准所有者不能表达资源的获取、释放 或错误策略时,使用所有者句柄是合适的:

cpp
#include <cstdio>
#include <utility>

class File {
public:
    explicit File(std::FILE* owned_handle) noexcept : handle_(owned_handle) {}
    ~File() { if (handle_) std::fclose(handle_); }

    File(const File&) = delete;
    File& operator=(const File&) = delete;
    File(File&& other) noexcept : handle_(std::exchange(other.handle_, nullptr)) {}
    File& operator=(File&& other) noexcept {
        if (this != &other) {
            if (handle_) std::fclose(handle_);
            handle_ = std::exchange(other.handle_, nullptr);
        }
        return *this;
    }

private:
    std::FILE* handle_;
};

栈展开期间析构函数不能抛出异常。当调用者必须观察释放失败时,提供显式的类似 close() 的操作,然后让析构函数执行不抛出异常的清理。确保自定义移动操作转移资源, 并使源对象处于可安全销毁的状态;默认的指针移动会复制裸指针,因此对于所有者裸句柄是 错误的。如果能够表达所有权,应优先使用带自定义删除器的 std::unique_ptr。

领域值应组合正确的所有者:

cpp
#include <string>
#include <utility>
#include <vector>

class Entry {
public:
    explicit Entry(std::string name) : name_(std::move(name)) {}

private:
    std::string name_;
    std::vector<std::string> labels_;
};

由于 std::string 和 std::vector 已经提供值语义,Entry 不需要自定义析构函数、 复制操作或移动操作。

用继承表示接口 #

公有继承表示接口层面的“是一个”关系。只有当基类的每个有效操作对派生类型仍然有效时, 才使用公有继承;若只是为了复用实现而不需要可替代性,则使用组合。

cpp
struct Shape {
    virtual ~Shape() = default;
    virtual double area() const = 0;
};

当客户端可以通过基类指针销毁派生对象时,多态基类需要虚析构函数。使用 override 标记 有意的重写;只有在有意关闭扩展边界时才使用 final。保持表示私有,因为允许访问可变 表示会像公开数据一样直接削弱不变量。

将参数作为所有权契约 #

在考虑复制成本之前,先根据所有权和生命周期要求选择参数形式:

参数契约
T被调用方接收自己的值;适合廉价值,或被调用方会保留副本的情况。
const T&非所有者只读借用;调用者在调用期间负责维持生命周期。
T&非空可变借用;被调用方可以修改被引用对象,但不拥有它。
T*可以为空的借用;空值必须具有文档化的含义。
std::unique_ptr<T> 按值传递所有权转移给被调用方。
std::shared_ptr<T> 按值传递建立或延长共享所有权;仅在确实需要共享生命周期时使用。

按值接收 std::string 并将其移动到存储中,可以为左值和右值调用者高效表达独立保留的值:

cpp
#include <string>
#include <utility>

class User {
public:
    explicit User(std::string name) : name_(std::move(name)) {}

private:
    std::string name_;
};

不要使用裸指针隐藏所有权转移。当调用者承担责任时,应返回 std::unique_ptr<T> 等所有者值;只有在 API 能够说明并维持被借用对象的生命周期时,才 返回引用或非所有者指针。

评论

在 GitHub 上阅读或评论(发表评论需要登录)