Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 240
Corristo Avatar asked May 27 '17 17:05

Corristo


People also ask

What is a structured binding declaration?

A structured binding declaration introduces all identifiers in the identifier-list as names in the surrounding scope and binds them to subobjects or elements of the object denoted by expression. The bindings so introduced are called structured bindings.

What is perfect forwarding?

What is Perfect Forwarding. Perfect forwarding allows a template function that accepts a set of arguments to forward these arguments to another function whilst retaining the lvalue or rvalue nature of the original function arguments.

What is forwarding reference in C++?

When t is a forwarding reference (a function argument that is declared as an rvalue reference to a cv-unqualified function template parameter), this overload forwards the argument to another function with the value category it had when passed to the calling function.

What is std :: forward used for?

std::forward has a single use case: to cast a templated function parameter (inside the function) to the value category (lvalue or rvalue) the caller used to pass it. This allows rvalue arguments to be passed on as rvalues, and lvalues to be passed on as lvalues, a scheme called “perfect forwarding.”


2 Answers

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

like image 135
Oktalist Avatar answered Sep 24 '22 00:09

Oktalist


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);
like image 20
T.C. Avatar answered Sep 22 '22 00:09

T.C.