Search code examples
c++language-lawyerc++17perfect-forwardingstructured-bindings

Perfect forwarding of variables declared with structured binding


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?


Solution

  • 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);