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:
#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:
#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.
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:
| Parameter | Contract |
|---|---|
T | The 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 value | Ownership transfers to the callee. |
std::shared_ptr<T> by value | Shared 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:
#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)