To make a class with private members (or non-public access) support structured binding, implement the tuple-like protocol:
- Specialize
std::tuple_size— number of elements. - Specialize
std::tuple_elementfor each index — element types. - Provide
get<N>()— access to values (member or free function via ADL).
Example: User → (id, name)
#include <cstddef>
#include <string>
#include <utility>
class User {
private:
int id_;
std::string name_;
public:
User(int id, std::string name)
: id_(id), name_(std::move(name)) {}
template <std::size_t Index>
decltype(auto) get() & {
if constexpr (Index == 0) return (id_);
else if constexpr (Index == 1) return (name_);
}
template <std::size_t Index>
decltype(auto) get() const & {
if constexpr (Index == 0) return (id_);
else if constexpr (Index == 1) return (name_);
}
template <std::size_t Index>
decltype(auto) get() && {
if constexpr (Index == 0) return std::move(id_);
else if constexpr (Index == 1) return std::move(name_);
}
template <std::size_t Index>
decltype(auto) get() const && {
if constexpr (Index == 0) return std::move(id_);
else if constexpr (Index == 1) return std::move(name_);
}
};
namespace std {
template <>
struct tuple_size<User> : std::integral_constant<std::size_t, 2> {};
template <>
struct tuple_element<0, User> {
using type = int;
};
template <>
struct tuple_element<1, User> {
using type = std::string;
};
} // namespace std
Usage:
User user{101, "Oleksii"};
auto& [id, name] = user;
name = "Dmytro";
const auto& [cid, cname] = user;
auto [id_copy, name_copy] = user;
auto [moved_id, moved_name] = User{202, "Temporary"};
Notes
Finding get. First a member e.get<N>(). If missing — a free function via ADL: get<N>(e). A free function is useful when you cannot change the class:
template <std::size_t Index>
decltype(auto) get(const User& u) {
if constexpr (Index == 0) return u.get_id();
else if constexpr (Index == 1) return u.get_name();
}
Parentheses in return (id_). With decltype(auto) this returns int& / const int& (an lvalue expression), not a copy. For && overloads, members are moved with std::move.
Four call categories. Without && / const &&, unpacking a temporary may copy instead of move, or fail to compile the way you expect.
In C++23 the four get overloads can collapse into one method via deducing this and std::forward_like — covered in a separate note.