9

I have a struct

template <typename T>
struct Demo {
    T x;
    T y;
};

and I'm trying to write a generic function similar to std::get for tuples that takes a compile-time index I and returns an lvalue reference to the I-th member of the struct if it is called with an lvalue DemoStruct<T> and a rvalue reference to the I-th member of the struct if it is called with an rvalue DemoStruct<T>.

My current implementation looks like this

template <size_t I, typename T> 
constexpr decltype(auto) struct_get(T&& val) {
    auto&& [a, b] = std::forward<T>(val);

    if constexpr (I == 0) {
        return std::forward<decltype(a)>(a);
    } else {
        return std::forward<decltype(b)>(b);
    }
}

However, this doesn't do what I expected, and always returns an rvalue-reference to T instead.

Here is a wandbox that shows the problem.

What is the correct way to return references to the struct members preserving the value category of the struct passed into the function?

EDIT: As Kinan Al Sarmini pointed out, auto&& [a, b] = ... indeed deduces the types for a and b to be non-reference types. This is also true for std::tuple, e.g. both

std::tuple my_tuple{std::string{"foo"}, std::string{"bar"}};
auto&& [a, b] = my_tuple;
static_assert(!std::is_reference_v<decltype(a)>);

and

std::tuple my_tuple{std::string{"foo"}, std::string{"bar"}};
auto&& [a, b] = std::move(my_tuple);
static_assert(!std::is_reference_v<decltype(a)>);

compile fine, even though std::get<0>(my_tuple) returns references, as shown by

std::tuple my_tuple{3, 4};
static_assert(std::is_lvalue_reference_v<decltype(std::get<0>(my_tuple))>);
static_assert(std::is_rvalue_reference_v<decltype(std::get<0>(std::move(my_tuple)))>);

Is this a language defect, intended or a bug in both GCC and Clang?

Corristo
  • 4,911
  • 1
  • 20
  • 36
  • `a` and `b` are deduced to a non-reference type, that's why you're receiving an rvalue reference. I'm not sure why that happens though. – Kinan Al Sarmini May 27 '17 at 17:51
  • I don't understand your confusion. As long as `val` is a reference, any "movement" out of its members will have an effect on the calling object. Why don't you follow the example of `std::get` and provide another overload which takes `const T&` and doesn't `move`. – DeiDei May 27 '17 at 18:17
  • @DeiDei `std::forward` isn't supposed to return an rvalue reference when provided a lvalue reference type, which `decltype(a)` should be if structured bindings with `auto&&` follows regular argument deduction rules. If I create another overload for `T const&`, because it is in a deduction context, the `T&&` overload would still be preferred over the `T const&` version for non-const lvalues. So I'd have to create overloads for all cv-qualified versions, i.e. `T&`, `T const&`, `T volatile&`, `T const volatile&` in order for it to work with all kinds of lvalues; that is a lot of code duplication. – Corristo May 27 '17 at 18:27
  • Have you tried it with tuples or manual structured binding? Ie is this specific to automatic structured binding from structs? – Yakk - Adam Nevraumont May 27 '17 at 18:54
  • @Yakk This doesn't seem to be specific to structured bindings with structs. I've updated the question with more information. – Corristo May 27 '17 at 19:17

2 Answers2

8

The behavior is correct.

decltype applied to a structured binding returns the referenced type, which for a plain struct is the declared type of the data member referred to (but decorated with the cv-qualifiers of the complete object), and for the tuple-like case is "whatever tuple_element returned for that element". This roughly models the behavior of decltype as applied to a plain class member access.

I cannot currently think of anything other than manually computing the desired type, i.e.:

using fwd_t = std::conditional_t<std::is_lvalue_reference_v<T>,
                                 decltype(a)&,
                                 decltype(a)>;
return std::forward<fwd_t>(a);
T.C.
  • 133,968
  • 17
  • 288
  • 421
  • " Given the type Ti designated by std::tuple_element::type, each vi is a variable of type "reference to Ti" initialized with the initializer, where the reference is an lvalue reference if the initializer is an lvalue and an rvalue reference otherwise; the referenced type is Ti. " -- How are the `v_i`s (aka `[a,b]`) *not references*? How can `decltype(a)` not be a reference, if `a` is denoted `v_1` and the standard seems to say that `v_i` are either rvalue or lvalue references? – Yakk - Adam Nevraumont May 27 '17 at 21:20
  • @Yakk They are (for the tuple-like case), but `decltype` pretends that they aren't. – T.C. May 27 '17 at 21:21
  • [`int a = 7; int& x = a; static_assert(std::is_same{}, "wut?");`](http://coliru.stacked-crooked.com/a/27d04d9ae28f7ce0)? `a` is an identifier of lvalue reference type. `decltype` should treat it as an identifier, why can it "pretend it isn't"? Why is it different than `x` above? – Yakk - Adam Nevraumont May 27 '17 at 21:23
  • @Yakk What I'm saying is that `decltype` pretends that [this particular kind of references](https://timsong-cpp.github.io/cppwp/dcl.type.simple#4.1) aren't references, not all references. – T.C. May 27 '17 at 21:25
  • ah, why in the world did they do that? Ah, because .2. Got it. – Yakk - Adam Nevraumont May 27 '17 at 22:13
5

Here is general solution to get what you want. It is a variation on std::forward that permits its template argument and its function argument to have unrelated types, and conditionally casts its function argument to an rvalue iff its template argument is not an lvalue reference.

template <typename T, typename U>
constexpr decltype(auto) aliasing_forward(U&& obj) noexcept {
    if constexpr (std::is_lvalue_reference_v<T>) {
        return obj;
    } else {
        return std::move(obj);
    }
}

I named it aliasing_forward as a nod to the "aliasing constructor" of std::shared_ptr.

You could use it like this:

template <size_t I, typename T> 
constexpr decltype(auto) struct_get(T&& val) {
    auto&& [a, b] = val;

    if constexpr (I == 0) {
        return aliasing_forward<T>(a);
    } else {
        return aliasing_forward<T>(b);
    }
}

DEMO

Oktalist
  • 14,336
  • 3
  • 43
  • 63
  • @KonstantinVladimirov There is a [proposal](http://wg21.link/P0847) that would add it to the standard, with the name `std::forward_like`. – Oktalist Jan 22 '19 at 16:39