Search code examples
c++c++17structured-bindings

How can I use a structured binding to copy a tuple-like object with elements whose type is T&?


The origin of this problem is that I'm designing a 2-dimensional container implemented by std::vector. The result type of operator[] is a proxy class that has a fixed number of elements, and then I want to use structured binding with this proxy class, just like std::array. This is a simple example for it:

template<size_t stride>
struct Reference{
    Container2D<stride>* container;
    size_t index;

    template<size_t I>
    decltype(auto) get(){
        return container->data()[I + index * stride];
    }
};
/* the object means `stride` elements in container, starting at `index * stride` */

template<size_t stride>
struct Container2D{
    std::vector<int>& data();
    /* implemented by std::vector, simplify the template argument T */
    Reference operator[](size_t index);
    /* operator[] just constructs an object of Reference */
    /* so it returns a rvalue */
};

namespace std{
    template<size_t stride>
    struct tuple_size<Reference<stride>>{
        static constexpr size_t value = stride;
    };
    template<size_t stride>
    struct tuple_element<Reference<stride>>{
        /* 2 choices: */
        /* first: tuple_element_t<...> = T */
        typedef int type;
    };
}

In this case, I tried:

Container2D<2> container;
/* init... */
auto [a, b] = container[0];
/* get a copy of each element */
auto& [c, d] = container[0];
/* compile error */

But the compiler said "Non-const lvalue reference to type 'Reference<...>' cannot bind to a temporary of type 'Reference<...>'"

So if I want to modify the element by structured binding, I have to:

template<size_t stride>
struct tuple_element<Reference<stride>>{
    /* 2 choices: */
    /* second: tuple_element_t<...> = T& */
    typedef int& type;
};

and then:

Container2D<2> container;
/* init... */
auto [a, b] = container[0];
/* get a reference to each element */
// auto& [c, d] = container[0];
/* still compile error, but who cares? */

But in this case, if I want to get a copy, I have to declare some variables to copy these reference variables. It's exactly not what I want. Is there some better way that can deal with these two situations easily and correctly?

The following is in addition to this question:

I know that the implementation of structured binding is:

"auto" [const] [volatile] [&/&&] "[" <vars> "]" "=" <expression>

and may be implemented as (in a tuple-like case, simplifying some edge cases):

auto [const] [volatile] [&/&&] e = <expression>;
std::tuple_element_t<0, std::remove_reference_t<decltype(e)>> var_0(get<0>(std::forward(e)));
std::tuple_element_t<1, std::remove_reference_t<decltype(e)>> var_1(get<1>(std::forward(e)));
...

in which the grammar implies you can replace the [a, b, c, ...] with some variable name like e, and then the type of a, b and c follows a weird deduction rule.

However, this anonymous variable is always not what we want, but the a, b and c will be. So why not ensure the type of a, b and c? It can just apply the cv-qualifier and ref-operator to the std::tuple_element_t<I, E> for a, b and c, use auto&& e and std::forward(e) for the expression, and others are treated as before.


Solution

  • This is a very old C++ wart dressed in new clothes:

    std::vector<bool> x;
    auto& rx = x[0]; // does not compile
    

    Proxies are second class citizens. It is incompatible to return by value from operator[] and bind it using structured bindings with auto&.

    There are no solutions without trade-offs.

    To get the auto& bindings to work as-is, there must something alive somewhere to which operator[] can return a reference (e.g. as a container member). That thing must behave differently when bound by auto& than by auto (e.g., when copied, it enters "copy" mode). It should be possible to do this, and make this exact usage work, but it will be unmaintainable.

    A more reasonable approach is to give up the auto& bindings. In this case, you can provide proxies that act in value-like and reference-like fashions, e.g. something like this:

    auto [a, b] = container[0]; // copy
    auto [a, b] = container[0].ref(); // reference-like
    

    To make this work, operator[] returns a proxy for which get() will return copies, and calling .ref() on it returns a proxy for which get() returns references.

    The addition to the question is fairly interesting in its own right. There are some interesting tensions in this language feature. I'm not on the committee, but I can name some motivations that would tilt in this direction: (1) consistency (2) different deduction semantics, (3) efficiency, (4) teachability, and (5) life

    Note that the addition in the question glosses over an important distinction. The bound names are not references, but aliases. They are new names for the thing which is pointed to. This is an important distinction because bitfields work with structured bindings, but references to them cannot be formed.

    By (1), I mean that, if tuple-like bindings were references, they now are different than structured bindings in the class case (unless we do that differently and compromise the feature on bitfields). We now have a very subtle inconsistency in how structured bindings work.

    By (2), I mean that, everywhere in the language, auto&& has one type deduction take place. If auto&& [...] translated into a version where the bound names were auto&& then there are N different deductions, with potentially different lvalue/rvalue-ness. That makes them more complex than they already are (which is pretty complex)

    By (3), I mean that, if we write auto [...] = ..., we expect a copy, but not N copies. In the example provided, it's little difference because copying the aggregate is the same as copying each of the members, but that's not an intrinsic property. The members could use the aggregate to share some common state, that they would otherwise need to own their own copy. Having more than one copy operation could be surprising.

    By (4), I mean that, you can teach someone structured bindings initially by saying "they work as if you replace [...] with an object name and the binding names are new names for the parts of that thing".