Using std::optional for Safe Nullable Returns in C++17
Learn how std::optional in C++17 gives inline, type‑safe nullable returns, see a worked example, and understand the trade‑offs before adopting it in your codebase.
29 Oct 2025, 03:48 UTC

The problem: sentinel values and raw pointers
Many C++ APIs need to signal “no result” without throwing. A common approach is to return a sentinel (e.g., -1 for an integer lookup) or a raw pointer that may be nullptr. Both techniques rely on the caller remembering a special value, and they can hide bugs when the sentinel is a legitimate result or when the pointer is dereferenced by mistake.
Thesis: std::optional gives inline, type‑safe optionality
std::optional<T> (available since C++17) stores a T object directly inside the optional plus a small boolean that tells whether a value is present. No dynamic allocation is performed for the optionality itself, and the type cannot be confused with a valid T value.
What it actually stores
For a type T, the size of std::optional<T> is at least sizeof(T) plus whatever padding the implementation needs for the engaged flag. It is never larger than sizeof(T) + 1 rounded to the natural alignment of T. You can verify this on your toolchain by printing sizeof(std::optional<int>) and sizeof(int).
Safe access patterns
has_value()or explicitboolconversion tells you if a value is present.value_or(default)returns the stored value if present, otherwise the supplied default—no exception.operator*andoperator->give direct access but only when you know the optional is engaged; otherwise the behavior is undefined.value()throwsstd::bad_optional_accesson an empty optional, providing a checked alternative tooperator*.
Worked example: optional string lookup
Suppose we have a configuration map and want to fetch a setting that may be absent.
#include <optional>
#include <string>
#include <unordered_map>
#include <iostream>
std::optional get_setting(
const std::unordered_map<std::string, std::string>& config,
const std::string& key)
{
auto it = config.find(key);
if (it != config.end()) {
return it->second; // copy/move into the optional
}
return std::nullopt; // explicitly empty
}
int main() {
std::unordered_map<std::string, std::string> cfg = {{\"host\", \"example.com\"}};
auto opt = get_setting(cfg, \"port\");
if (opt) {
std::cout << \"Port: \" << *opt << '\\n';
} else {
std::cout << \"Using default port 80\\n';
std::cout << \"Port: \" << opt.value_or(\"80\") << '\\n';
}
// Demonstrating checked access that throws on empty
try {
std::cout << opt.value() << '\\n'; // will throw
} catch (const std::bad_optional_access&) {
std::cout << \"Caught bad_optional_access as expected\\n';
}
}
Compile with a C++17‑compatible compiler:
g++ -std=c++17 -Wall -Wextra example.cpp -o example
Running the program prints:
Using default port 80\nPort: 80\nCaught bad_optional_access as expected\n
Trade‑offs and limitations
- Size overhead. For small trivially copyable types the extra byte (or padding) may be noticeable in tight loops. Measure
sizeof(std::optional<T>)againstsizeof(T)for your target. - Copy/move cost. Returning
std::optional<T>by value copies or moves the wholeTwhen engaged. If absence is rare andTis large, consider alternatives likestd::expected(C++23) or returning a pointer/reference with a separate error code. - Not a reference substitute.
std::optional<T&>exists but has rebind semantics that can surprise; use a plainT&when the reference is required and only wrap when you truly need reseatability or absence. - Compiler support. Pre‑C++17 toolchains may only provide
std::experimental::optionalor nothing at all. Verify with#if __cplusplus >= 201703Lor check your standard library’s documentation.
Actionable closing
When you need a function that may return “no result” and the absence is a normal, expected outcome:
- Include
<optional>and compile with-std=c++17(or/std:c++17on MSVC). - Return
std::optional<T>by value. - Check presence with
if (opt)oropt.has_value()before using*optoropt.value(). - Provide a sensible default with
value_orwhen appropriate. - Measure
sizeof(std::optional<T>)in your codebase to ensure the overhead fits your performance budget.
By adopting std::optional you replace error‑prone sentinels and raw pointers with a type‑safe, self‑documenting alternative that keeps the value inline and avoids heap allocation for the optionality itself.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.