C++ Structured Binding for Custom Classes
Structured binding in C++17 allows developers to decompose objects
into individual variables with a clean, expressive syntax. While simple
aggregate types with public member variables work out of the box, types
with private data members or non-trivial interfaces require implementing
the "tuple-like protocol." This mechanism enables custom classes to
participate in structured bindings by specializing
std::tuple_size and std::tuple_element,
alongside implementing an accessible get function
template.
The Tuple-Like Protocol Overview
When the compiler encounters a structured binding declaration such as
auto [a, b] = my_object;, it inspects the type to determine
how to perform the decomposition. For non-aggregate types or types
explicitly opting into customization, the compiler checks for the
existence of std::tuple_size<T>. If present, it
treats the type as tuple-like and strictly requires three
components:
std::tuple_size<T>specialization: Specifies the exact number of elements returned by the decomposition.std::tuple_element<I, T>specialization: Defines the type of each element corresponding to indexI.get<I>()overload: Accesses the underlying element at indexI, implemented either as a member function template or as a free function template found via Argument-Dependent Lookup (ADL).
Step 1: Specialize
std::tuple_size
The std::tuple_size template must be specialized in
namespace std. It must inherit from
std::integral_constant<std::size_t, N>, where
N is the fixed number of elements exposed for structured
binding.
#include <cstddef>
#include <type_traits>
class Coordinate {
private:
double x_;
double y_;
public:
Coordinate(double x, double y) : x_(x), y_(y) {}
// Friends or member methods provide access to data
template <std::size_t Index>
friend auto& get(Coordinate& c);
template <std::size_t Index>
friend const auto& get(const Coordinate& c);
};
template <>
struct std::tuple_size<Coordinate> : std::integral_constant<std::size_t, 2> {};Step 2: Specialize
std::tuple_element
The compiler needs to know the type produced at each position
I during decomposition. Specialize
std::tuple_element for each index from 0 to
N - 1:
template <std::size_t Index>
struct std::tuple_element<Index, Coordinate> {
using type = double;
};If the types differ across elements, you can use conditional type definitions or provide explicit specializations for each index:
template <>
struct std::tuple_element<0, CustomType> {
using type = int;
};
template <>
struct std::tuple_element<1, CustomType> {
using type = std::string;
};Step 3: Implement the
get Template
The get function can be defined either as a member
template (e.g., c.get<0>()) or as a free function
template in the same namespace as the custom type so that ADL can locate
it. Free functions are generally preferred as they keep the class
interface clean and mirror the standard library conventions.
Using C++17 if constexpr, the implementation can handle
multiple indices compactly:
template <std::size_t Index>
auto& get(Coordinate& c) {
if constexpr (Index == 0) return c.x_;
else if constexpr (Index == 1) return c.y_;
}
template <std::size_t Index>
const auto& get(const Coordinate& c) {
if constexpr (Index == 0) return c.x_;
else if constexpr (Index == 1) return c.y_;
}
template <std::size_t Index>
auto&& get(Coordinate&& c) {
if constexpr (Index == 0) return std::move(c.x_);
else if constexpr (Index == 1) return std::move(c.y_);
}Providing overloads for non-const references, const
references, and rvalue references ensures that structured bindings
behave correctly whether bound via auto [x, y],
const auto& [x, y], or
auto&& [x, y].
Value and Reference Binding Semantics
When structured binding syntax is executed:
Coordinate point{10.5, 20.0};
auto& [x_ref, y_ref] = point;
x_ref = 15.0; // Modifies point.x_ directlyThe compiler creates an invisible, hidden variable that holds the
bound expression (either as a copy or reference depending on
auto, auto&, or
auto&&). The named identifiers (x_ref,
y_ref) are aliases referencing the results of the
corresponding get<0>() and
get<1>() invocations on that hidden variable.
By fulfilling the tuple-like protocol through
tuple_size, tuple_element, and
get, custom classes achieve full integration with C++
structured bindings while strictly preserving encapsulation.