Frank
Frank

Reputation: 160

Structure bindings for class data accessed through member functions

Stroustrup mentions in his book A Tour of C++ (section 3.6.3) that it is possible to do structured bindings for a class whose data is accessible through member functions. Here's the example code he gives:

complex<double> z = {1,2}
auto [re,im] = z+2

Then he says,

Mapping a complex<double> to two local variables, such as re and im is feasible and efficient, but the technique for doing so is beyond the scope of this book.

What is he referring to by "the technique for doing so"?

Upvotes: 2

Views: 113

Answers (1)

HolyBlackCat
HolyBlackCat

Reputation: 96906

Check out cppreference.

You specialize std::tuple_size<T> and std::tuple_element<T> for your type, and provide get<I>(...) either in the same namespace (where it can be found by ADL) or as a member function.

get needs 4 overloads: for T &, const T &, T &&, and const T &&, though in some cases you might be able to get away with less overloads.

tuple_size and tuple_element only need one specialization each. You don't need to manually specialize for const and/or volatile T, since the standard library already defines generic specializations for const and/or volatile types that forward to the non-qualified specializations.

Example:

#include <cstddef>
#include <iostream>
#include <type_traits>
#include <utility>

struct A
{
    int unused;
    int x;
    float y;
    std::string z;

    template <std::size_t I>
    friend auto &get(A &a)
    {
        static_assert(I < 3);
        if constexpr (I == 0)
            return a.x;
        else if constexpr (I == 1)
            return a.y;
        else
            return a.z;
    }

    template <std::size_t I>
    friend const auto &get(const A &a)
    {
        return get<I>(const_cast<A &>(a));
    }

    template <std::size_t I>
    friend auto &&get(A &&a)
    {
        return get<I>(static_cast<A &>(a));
    }

    template <std::size_t I>
    friend const auto &&get(const A &&a)
    {
        return get<I>(const_cast<A &&>(a));
    }
};

template <>
struct std::tuple_size<A> : std::integral_constant<std::size_t, 3> {};

template <std::size_t I>
struct std::tuple_element<I, A>
{
    static_assert(I < 3);
    using type =
        std::conditional_t<I == 0, int,
        std::conditional_t<I == 1, float,
                                   std::string>>;
};

auto [f, g, h] = A{};

Variation 1: get defined outside of the class.

struct A
{
    int unused;
    int x;
    float y;
    std::string z;
};

template <std::size_t I>
auto &get(A &a)
{
    static_assert(I < 3);
    if constexpr (I == 0)
        return a.x;
    else if constexpr (I == 1)
        return a.y;
    else
        return a.z;
}

template <std::size_t I>
const auto &get(const A &a)
{
    return get<I>(const_cast<A &>(a));
}

template <std::size_t I>
auto &&get(A &&a)
{
    return get<I>(static_cast<A &>(a));
}

template <std::size_t I>
const auto &&get(const A &&a)
{
    return get<I>(const_cast<A &&>(a));
}

Variation 2: get is a non-static member function.

struct A
{
    int unused;
    int x;
    float y;
    std::string z;

    template <std::size_t I>
    auto &get() &
    {
        static_assert(I < 3);
        if constexpr (I == 0)
            return x;
        else if constexpr (I == 1)
            return y;
        else
            return z;
    }

    template <std::size_t I>
    const auto &get() const &
    {
        return const_cast<A *>(this)->get<I>();
    }

    template <std::size_t I>
    auto &&get() &&
    {
        return get<I>();
    }

    template <std::size_t I>
    const auto &&get() const &&
    {
        return get<I>();
    }
};

Upvotes: 3

Related Questions