Structured bindings, [[std::optional]] and [[std::variant]] are three C++17 features that solve the same family of problems: expressing "a value that may be one of several things" or "a value that may not be there" in the type system. Together they remove a large class of sentinel values, out-parameters and awkward tuples from ordinary code.
Structured bindings name the parts
A structured binding lets you destructure a pair, a tuple or a map entry into named variables.
#include
#include
#include
#include
std::tuple reading() {
return {42, "oil", 91.5};
}
int main() {
auto [value, label, percentage] = reading();
std::cout << value << ' ' << label << ' ' << percentage << '\n';
std::pair entry = {"coolant", 30};
auto& [name, count] = entry;
std::cout << name << '=' << count << '\n';
return 0;
}The output shows the tuple and the pair being unpacked into readable names:
42 oil 91.5
coolant=30There are three forms. auto [a, b] copies, auto& [a, b] binds references, and const auto& [a, b] binds const references. The last is the one to reach for when you only need to read.
Structured bindings with maps
They work particularly well with a map, where the entry is a pair of a key and a value.
#include
#include
#include
int main() {
std::map weights = {
{"p-001", 4}, {"p-002", 30}, {"p-003", 12}
};
for (const auto& [id, weight] : weights) {
std::cout << id << '=' << weight << ' ';
}
std::cout << '\n';
auto it = weights.find("p-002");
if (it != weights.end()) {
const auto& [id, weight] = *it;
std::cout << "found " << id << " with " << weight << '\n';
}
return 0;
}The output shows both the whole map and a single found entry read clearly:
p-001=4 p-002=30 p-003=12
found p-002 with 30optional for a value that may not be there
std::optional holds a value or nothing. It replaces the sentinel values that every codebase otherwise accumulates: -1, 0, nullptr, an empty string.
#include
#include
#include
std::optional find_weight(const std::string& id) {
if (id == "p-002") {
return 30;
}
return std::nullopt;
}
int main() {
auto found = find_weight("p-002");
if (found.has_value()) {
std::cout << "weight: " << *found << '\n';
} else {
std::cout << "not found\n";
}
auto missing = find_weight("p-009");
std::cout << "missing has value: " << missing.has_value() << '\n';
std::cout << "value or default: " << missing.value_or(-1) << '\n';
return 0;
}The output shows the two cases and the fallback:
weight: 30
missing has value: 0
value or default: -1The important difference from a sentinel is that the compiler forces you to check. With an int return value, the caller cannot tell "weight is zero" from "nothing found". With an optional, the two states are distinct in the type.
optional with references and arguments
An optional can hold a reference, which is useful for returning a reference to something that may not exist.
#include
#include
#include
#include
int main() {
std::map weights = {{"p-001", 4}};
std::optional> found = [&]() -> std::optional> {
auto it = weights.find("p-001");
if (it == weights.end()) {
return std::nullopt;
}
return std::ref(it->second);
}();
if (found.has_value()) {
std::cout << "weight: " << found->get() << '\n';
}
return 0;
}The output shows the reference being read through the optional:
weight: 4This is more machinery than most code needs, which is why returning a pointer or an std::map::iterator is often the more readable choice for "may not be there".
variant for a value of several types
std::variant holds one value chosen from a fixed list of types, and the alternative is part of the type.
#include
#include
#include
using Reading = std::variant;
std::string describe(const Reading& reading) {
if (std::holds_alternative(reading)) {
return "numeric: " + std::to_string(std::get(reading));
}
if (std::holds_alternative(reading)) {
return "text: " + std::get(reading);
}
return std::string("flag: ") + (std::get(reading) ? "true" : "false");
}
int main() {
std::cout << describe(42.5) << '\n';
std::cout << describe(std::string("unknown")) << '\n';
std::cout << describe(true) << '\n';
return 0;
}The output shows all three alternatives being handled:
numeric: 42.500000
text: unknown
flag: trueA variant is the type-safe replacement for a union with a manual tag. The alternative index is tracked by the library, so the tag and the value can never disagree.
Choosing between them
- Structured bindings — when you have a pair, a tuple or a map entry and want readable names for the parts.
std::optional— when a function may or may not produce a value, and a sentinel would be ambiguous.std::variant— when a value is genuinely one of several types, and you want the compiler to check every branch.- A pointer — when the "not there" case is naturally a null pointer and the target is a class type.
- A plain value — when the function always succeeds and a separate success flag is not needed.
Quick reference
- Structured bindings unpack a pair, tuple or map entry into named variables.
- Use
const auto& [name, value] to read a map without copying. std::optionalholds a value or nothing; check with.has_value()..value_or(default)supplies a fallback, and*reads the value.std::variantholds one of several types; check withstd::holds_alternativeand read withstd::get.- Both optional and variant make the "may not be there" case part of the type.
Try it yourself: write a function that looks up a parcel weight and returns a [[std::optional]] instead of a sentinel [[-1]], and call it for a parcel that exists and one that does not. Next, return a pair of the identifier and the weight from a lookup and unpack it with a structured binding. Finally, build a [[std::variant]] that can hold a weight, a "not measured" marker or a text note, and handle each alternative.