C++ 設計指引

最後更新: 2023-02-05 10:36:28 +0800 +0800
原文以英文撰寫。您正在閱讀AI翻譯版本。

C++ 介面應在型別與簽章中編碼擁有權、生命週期與可替代性。C++ Core Guidelines 的零法則正是源自這項設計:由 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*可以為 null 的借用;null 具有已文件化的意義。
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 上閱讀或評論(發表評論需要登入)