The tuple-like protocol for structured binding usually needs four get overloads: &, const &, &&, const &&. In C++23, deducing this replaces them with one templated method, and std::forward_like (<utility>) forwards each member’s value category.
Implementation
#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, typename Self>
constexpr decltype(auto) get(this Self&& self) noexcept {
if constexpr (Index == 0) {
return std::forward_like<Self>(self.id_);
} else if constexpr (Index == 1) {
return std::forward_like<Self>(self.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; };
}
tuple_size / tuple_element stay as before.
How Self is deduced
this Self&& self is a forwarding reference to the object of the call:
| Called on | Self |
|---|---|
User& |
User& |
const User& |
const User& |
temporary User&& |
User (then Self&& → User&&) |
Why std::forward_like
std::forward<Self>(self) forwards self itself. You need to forward a member (self.id_, self.name_) with the same constness and value category as Self.
That is what std::forward_like<Self>(field) does:
Self |
result for a std::string member |
|---|---|
User& |
std::string& |
const User& |
const std::string& |
User / User&& |
std::string&& |
Compared with hand-written overloads: one template and forward_like instead of four methods and manual std::move / (field).
More on std::forward_like and replacing CRTP with deducing this follows in the next notes.