Choose parameter and return types to communicate intent about optionality directly.
Unintentional or unclear optionality in your API can lead to confusion, unnecessary complexity (e.g., redundant nullptr checks), or even runtime errors and bugs. By explicitly choosing types that reflect whether a value is required or optional, you make your code’s intent self-documenting and reduce the risk of misuse.
For input parameters
Section titled “For input parameters”T: No optionality, preferred default for value typesT&orconst T&: No optionality, argument is required and borrowed, common for large value types to avoid copiesstd::optional<T>: Argument itself is optional and modeled with value semantics, common for value types where absence is a valid stateT*: Argument is optional borrowed access (nullptrmeans “not provided”), non-owning by convention, common for pointer types or when nullable access is intentional and ownership is elsewhere. Common for historical reasons. Consider usingstd::optional<std::reference_wrapper<T>>instead for optional references to avoid raw pointer semantics and clarify intent.std::optional<T&>: Invalid type, usestd::optional<std::reference_wrapper<T>>instead to express optional references.std::optional<std::reference_wrapper<T>>: Optional reference, use when you want to express optional borrowing of an existing object without ownership semantics.
void render(Config cfg); // Required copied input (no optionality)void render(const Config& cfg); // Required borrowed input (no optionality), common for large value types to avoid copiesvoid render(std::optional<Config> cfg); // Optional value input, caller can pass std::nullopt to indicate absence.void render(const Config* cfg); // Optional borrowed input (nullable)For return values
Section titled “For return values”std::optional<T>: operation may or may not produce a value, common for functions that might not have a meaningful result in some cases (e.g., lookup functions)std::expected<T, E>: operation produces a value or an explicit error, common for functions that can fail and callers need to know why.std::unique_ptr<T>: use when transferring ownership of a heap object, common for factory functions.
auto loadConfig(Path p) -> std::optional<Config>; // Optional result with value semanticsauto parseConfig(Path p) -> std::expected<Config, Error>; // Value or explicit errorauto makeNode() -> std::unique_ptr<Node>; // Ownership transfer or nullptrCheat sheet
Section titled “Cheat sheet”| Type | Intent | Ownership | Nullable? |
|---|---|---|---|
T | Required value, passed by copy | _ | No |
T& | Required reference to existing object | No | No |
T* | Optional reference to existing object | No | Yes |
std::optional<T> | Optional value, passed by copy | _ | Yes |
std::unique_ptr<T> | Optional pointer with ownership of a dynamically allocated object | Yes | Yes |
std::expected<T,E> | Either a value T or an error E (never empty) | _ | No |
std::optional<T&> | Optional reference. Invalid type, use std::optional<std::reference_wrapper<T>> instead | N/A (invalid type) | N/A |
std::optional<std::reference_wrapper<T>> | Optional reference | Borrows resource | Yes |
Core Guidelines links
Section titled “Core Guidelines links”- I.11: Never transfer ownership by a raw pointer (
T*) or reference (T&) - F.60: Prefer
T*overT&when “no argument” is a valid option