4

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.

Boann
  • 48,794
  • 16
  • 117
  • 146
RedFog
  • 1,005
  • 4
  • 10
  • The problem is in your `get()`. `auto` return type decays, returning a copy of the element. Try using `decltype(auto) get(...)` Edit: confused `get` with `operator[]` To be clear: right now, by the time you write container[0], you have no way to modify its elements through the `Reference`, with structured bindings or without them. – shananton Aug 13 '20 at 13:32
  • @shananton sorry, it's a mistake in example. thanks for pointing out. – RedFog Aug 13 '20 at 13:35
  • I now realized you knew the rules for structured bindings already :facepalm: Ok I'll go cry and try to think of an actual solution, because now I'm also curious about this :^) – shananton Aug 13 '20 at 13:42
  • You basically want it to seem like the `auto[&]` in the structured binding declaration applies to the individual variables, right? – shananton Aug 13 '20 at 13:46
  • @shananton nope. I want to decide the tuple_element_t> to make me able to get the reference of each element, and able to get a copy as well. just like `auto [x, y] = expr;` and `auto& [x, y] = expr;` do. – RedFog Aug 13 '20 at 13:49
  • Now I don't really get it... `auto [x, y] = container[0];` should copy the elements, correct? What about `auto& [x, y] = container[0];`? Should this give you a reference to each element? – shananton Aug 13 '20 at 13:53
  • yes, it's the best in my hope if `auto` get a copy and `auto&` get a reference. and even if not, it's acceptable in some ways if I can get copy and reference without any other variable declarations. – RedFog Aug 13 '20 at 14:06

1 Answers1

2

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".

Jeff Garrett
  • 5,863
  • 1
  • 13
  • 12
  • thanks for your answer, the solution with `.ref()` is acceptable for me. and then I can understand what you means, in which I forget bitfields and separate copies may do different performances. however I think it's still correct if bound names are different copies from the same reference instead of the same references to a copy object when I want to copy it, and I also think it's awkward that bitfields make these names aliases, which causes weird performance in deduction of `decltype` and in lambda expression capture. but it's enough, I have got the idea how it's designed. – RedFog Aug 14 '20 at 02:12