5

I want to provide structured binding for an inner class of a class template. How can I specialise std::tuple_size for that inner class?

I can't use structured binding to data member, because the inner class may be a type incompatible with that feature. So I need to provide tuple-like structured binding. To provide such a feature to the inner class I need to partially specialise std::tuple_size in namespace std. The problem is that I got a non-deduced context for the parameter T (of the outer class).

I know that I could put the inner class in the global namespace and thus solve every problem, but is there any way to get the same result keeping the class inner?

#include <tuple>

template<typename T>
class Collection
{
    public:
        struct Element
        {
            int id;
            T actual_element;
        };

    //...
};


namespace std 
{
    template<typename T>
    struct tuple_size<typename Collection<T>::Element> // Error! Non-deduced context
        : std::integral_constant<std::size_t, 2> {};
}



//Collection iterators...

int main()
{
    Collection<int> collection;

    for (auto & [id, element] : collection) //what I'd like to achieve
        //do some stuff...
}
Tarquiscani
  • 649
  • 7
  • 20

2 Answers2

4

You don't have to provide a binding for this case: Element is already decomposable as is:

struct Element { int i, j; };
auto [i, j] = Element{2, 3}; // ok

However, assuming Element is actually more complex and needs custom bindings, then yes - you will need to move it out. However, it doesn't need to be in a global namespace. It could be somewhere else:

namespace detail {
    template <typename T> struct Element { ... };
}

template<typename T>
class Collection
{
    public:
        using Element = detail::Element<T>;
        friend Element;
        // ...
};

And at that point, specializing the bindings is straightforward. There is no way around that since, as you point out, specializing on Collection<T>::Element is a non-deduced context.


Short of a new language feature that would let you opt-in to structured bindings within the class body itself. There was such a paper for this, P1096, but it was rejected when presented. Which isn't to say a new proposal couldn't do better.

Barry
  • 286,269
  • 29
  • 621
  • 977
0

I do not claim the below solution to be the most elegant (or elegant at all) solution for the problem. It, however, achieves to add structure binding support for the inner class Element of the template class Collection by using/exploiting a C++20 concept for this very specific purpose.

Given that the get member functions, can be declared as free functions as well, it is possible to use the below pattern for adding structure binding support for inner classes that cannot/may not be modified (e.g., legacy, 3th party). The concept should be tweaked accordingly.

Try It Online

Code:

#include <concepts>
#include <tuple>

struct Hack
{};

template< typename T >
concept CollectionElement = requires
{
   typename T::type;
   { T::hack } -> std::same_as< const Hack& >;
};

namespace std
{
  template< ::CollectionElement T >
  class tuple_size< T > : public integral_constant< size_t, 2 >
  {};

  template< ::CollectionElement T >
  struct tuple_element< 0u, T >
  {
      using type = int;
  };

  template< ::CollectionElement T >
  struct tuple_element< 1u, T >
  {
      using type = typename T::type;
  };
}

template<typename T>
class Collection
{
    public:

        struct Element
        {
            static constexpr Hack hack = {};
            using type = T;

            template< std::size_t I >
            std::tuple_element_t< I, Element >& get() &
            {
                if constexpr (I == 0u) return id;
                if constexpr (I == 1u) return actual_element;
            }

            template< std::size_t I >
            const std::tuple_element_t< I, Element >& get() const&
            {
                if constexpr (I == 0u) return id;
                if constexpr (I == 1u) return actual_element;
            }

            template< std::size_t I >
            std::tuple_element_t< I, Element >& get() &&
            {
                if constexpr (I == 0u) return id;
                if constexpr (I == 1u) return actual_element;
            }

            template< std::size_t I >
            const std::tuple_element_t< I, Element >& get() const&&
            {
                if constexpr (I == 0u) return id;
                if constexpr (I == 1u) return actual_element;
            }
            
            int id;
            T actual_element;
        };

    //...
};

int main()
{
    Collection< int >::Element test
    {
        .id = 3,
        .actual_element = 5
    };

    auto& [id, element] = test;
    id = 7;
    element = 9;
    
    return id + element; // returns 16
}

General principle:

#include <concepts>
#include <tuple>

template< typename T >
struct Outer
{
    using value_type = T;

    struct Inner
    {
        using value_type = typename Outer::value_type;

        int m_first  = {};
        int m_second = {};
    };
};

namespace std
{
  template< typename T >
  requires(std::same_as< T, typename ::Outer< typename T::value_type >::Inner >)
  class tuple_size< T > : public integral_constant< size_t, 2 >
  {};

  template< typename T >
  requires(std::same_as< T, typename ::Outer< typename T::value_type >::Inner >)
  struct tuple_element< 0u, T >
  {
      using type = int;
  };

  template< typename T >
  requires(std::same_as< T, typename ::Outer< typename T::value_type >::Inner >)
  struct tuple_element< 1u, T >
  {
      using type = int;
  };
}

template< std::size_t I, typename T >
requires(std::same_as< T, typename Outer< typename T::value_type >::Inner >)
[[nodiscard]]
inline std::tuple_element_t< I, T >& get(T& src)
{
    if constexpr (I == 0u)
    {
        return src.m_first;
    }
    if constexpr (I == 1u)
    {
        return src.m_second;
    }
}

template< std::size_t I, typename T >
requires(std::same_as< T, typename Outer< typename T::value_type >::Inner >)
[[nodiscard]]
inline const std::tuple_element_t< I, T >& get(const T& src)
{
    if constexpr (I == 0u)
    {
        return src.m_first;
    }
    if constexpr (I == 1u)
    {
        return src.m_second;
    }
}

template< std::size_t I, typename T >
requires(std::same_as< T, typename Outer< typename T::value_type >::Inner >)
[[nodiscard]]
inline std::tuple_element_t< I, T >&& get(T&& src)
{
    if constexpr (I == 0u)
    {
        return src.m_first;
    }
    if constexpr (I == 1u)
    {
        return src.m_second;
    }
}

template< std::size_t I, typename T >
requires(std::same_as< T, typename Outer< typename T::value_type >::Inner >)
[[nodiscard]]
inline const std::tuple_element_t< I, T >&& get(const T&& src)
{
    if constexpr (I == 0u)
    {
        return src.m_first;
    }
    if constexpr (I == 1u)
    {
        return src.m_second;
    }
}

int main()
{
    Outer< int >::Inner ref;
    auto& [f, s] = ref;
    f = 3; s = 5;
    
    return ref.m_first + ref.m_second;
}
Matthias
  • 4,481
  • 12
  • 45
  • 84