C++ Design Guidelines

Last update: 2023-02-05 10:36:28 +0800 +0800
Available translations: 日本語 / 中文(简体) / 中文(繁體)

C++ interfaces should encode ownership, lifetime, and substitutability in types and signatures. The C++ Core Guidelines’ Rule of Zero follows from this design: a domain value composed from RAII members should not manually reproduce their copying, moving, and destruction behavior.

Resource Owners and Domain Values #

A resource-owning type acquires one resource, releases it in its destructor, and states whether copying or transfer is permitted. A handle represents access to a resource; an owning handle also bears responsibility for release. It is appropriate when a standard owner does not represent the resource’s acquisition, release, or error policy:

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_;
};

A destructor must not throw while stack unwinding. Provide an explicit close()-like operation when callers must observe a release failure, then leave the destructor to perform non-throwing cleanup. Ensure a custom move operation transfers the resource and leaves the source safe for destruction; defaulted pointer moves copy a raw pointer and are therefore incorrect for an owning raw handle. Prefer std::unique_ptr with a custom deleter when it can express the ownership.

A domain value should compose correct owners:

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_;
};

Because std::string and std::vector already provide value semantics, Entry needs no custom destructor or copy and move operations.

Inheritance for Interfaces #

Public inheritance models an interface-level “is-a” relationship. Use it only when every operation valid for the base remains valid for the derived type; use composition for implementation reuse without substitutability.

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

A polymorphic base requires a virtual destructor when clients can destroy a derived object through a base pointer. Mark intended overrides with override; apply final only where the extension boundary is intentionally closed. Keep representation private because mutable representation access weakens invariants as directly as public data does.

Parameters as Ownership Contracts #

Select a parameter form from ownership and lifetime requirements before considering copy cost:

ParameterContract
TThe callee receives its own value; suitable for cheap values or when the callee retains a copy.
const T&Non-owning read-only borrow; the caller retains lifetime responsibility through the call.
T&Non-null mutable borrow; the callee may modify the referred object but does not own it.
T*Borrowing that can be null when null has a documented meaning.
std::unique_ptr<T> by valueOwnership transfers to the callee.
std::shared_ptr<T> by valueShared ownership is established or prolonged; use only when shared lifetime is required.

Taking std::string by value and moving it into storage expresses an independent retained value efficiently for both lvalue and rvalue callers:

cpp
#include <string>
#include <utility>

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

private:
    std::string name_;
};

Do not use a raw pointer to conceal ownership transfer. Return an owning value such as std::unique_ptr<T> when the caller becomes responsible, and return a reference or non-owning pointer only while the API can state and uphold the borrowed object’s lifetime.

Comments

Read or comment on GitHub (sign-in required to comment)