9

I'm trying to support tuple-like structured binding access for a class. For simplicity, I'll use the following class in the rest of this post:

struct Test
{
    int v = 42;
};

(I'm aware that this class supports structured bindings out of the box but let's assume it does not.)

To enable tuple-like access to the member of Test, we must specialize std::tuple_size and std::tuple_element:

namespace std
{

template<>
struct tuple_size<Test>
{
    static const std::size_t value = 1;
};

template<std::size_t I>
struct tuple_element<I, Test>
{
    using type = int;
};

}

And the last part we need is either Test::get<i> or a function get<i>(Test) in Test's namespace. Let's implement the latter:

template<std::size_t I>
int get(Test t)
{
    return t.v;
}

This works. However, I would like to return a reference to Test's member, just like std::get(std::tuple), for example. Therefore, I implement get as follows:

template<std::size_t I>
int& get(Test& t)
{
    return t.v;
}

template<std::size_t I>
const int& get(const Test& t)
{
    return t.v;
}

With this version, however, the following code

auto test = Test{};
auto [v] = test;

produces an error (GCC 7.1):

binding reference of type ‘std::tuple_element<0, Test>::type& {aka int&}’ to ‘const int’ discards qualifiers

So it seems as if the get<i>(const Test&) overload is selected for the structured binding. Since this overload returns a const int&, and v acts like a non-const reference to int, the code fails to compile.

According to this, however, the line auto [v] = test; should be roughly equivalent to

auto e = test;
std::tuple_element<0, Test>::type& v = get<0>(e)

Which does work since it uses the get<i>(Test&) overload.

Any ideas on why my implementation of get does not work for structured bindings?

mtvec
  • 17,846
  • 5
  • 52
  • 83
  • 2
    You forgot "In these initializer expressions, `e` is an lvalue if the type of the entity `e` is an lvalue reference and an xvalue otherwise". – T.C. Aug 15 '17 at 18:20
  • @Barry The kind of the reference is determined from the value category of the initializer, which is an lvalue (because it ends up using the `const &` overload). Not sure how you want this to be diagnosed... – T.C. Aug 15 '17 at 18:41
  • @T.C. Yeah, deleted my comment before you even replied :) – Barry Aug 15 '17 at 18:51

1 Answers1

7

The problem is that auto [v] is a non-reference declaration, so test is copied and the copy of test is passed to get as an xvalue.

So you need to add an rvalue qualified get:

template<std::size_t I>
int&& get(Test&& t)
{
    return std::move(t.v);
}
ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • This indeed seems to solve the problem. However, what happens if multiple values are bound and `get` is called multiple times on the same copy of `test`? It seems to me that, since the copy is passed as an rvalue reference, it may be moved from resulting in undefined behavior the second time `get` is called. – mtvec Aug 15 '17 at 19:06
  • @Job `get` will be called multiple times on the same object, but with different values of `I`. It's your responsibility to ensure that these calls don't conflict. – ecatmur Aug 15 '17 at 19:11
  • I see, although it does seem error-prone to me. Is this documented somewhere in the standard? Also, is there any reason why the copy isn't simply passed as an lvalue reference? – mtvec Aug 15 '17 at 19:18
  • @Job no, the Standard doesn't call out this as a potential pitfall; I think that most people would consider a `get<>` that modifies elements other than the indicated one to be broken anyway. Your second question is a very good one (and possibly worthy of a separate SO question); all I can think is that the authors are allowing for a case where rvalue `get` has different (more efficient) behavior to lvalue `get`. Perhaps `get` involves a computation, and rvalue `get` can return a prvalue materializaing that computation. I'm having trouble thinking of a concrete example, though. – ecatmur Aug 15 '17 at 19:58