Skip to content

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.

  • T: No optionality, preferred default for value types
  • T& or const T&: No optionality, argument is required and borrowed, common for large value types to avoid copies
  • std::optional<T>: Argument itself is optional and modeled with value semantics, common for value types where absence is a valid state
  • T*: Argument is optional borrowed access (nullptr means “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 using std::optional<std::reference_wrapper<T>> instead for optional references to avoid raw pointer semantics and clarify intent.
  • std::optional<T&>: Invalid type, use std::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 copies
void 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)
  • 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 semantics
auto parseConfig(Path p) -> std::expected<Config, Error>; // Value or explicit error
auto makeNode() -> std::unique_ptr<Node>; // Ownership transfer or nullptr
TypeIntentOwnershipNullable?
TRequired value, passed by copy_No
T&Required reference to existing objectNoNo
T*Optional reference to existing objectNoYes
std::optional<T>Optional value, passed by copy_Yes
std::unique_ptr<T>Optional pointer with ownership of a dynamically allocated objectYesYes
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>> insteadN/A (invalid type)N/A
std::optional<std::reference_wrapper<T>>Optional referenceBorrows resourceYes