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.