Sam Coutteau
Sam Coutteau

Reputation: 417

Structured binding, passing them to other functions and value categories

The main question I have is:

How do you correctly write a function which applies structured binding and passes the resulting variables to a function, whilst correctly preserving references, const-ness, etc...

In Code

template<?>
<?> call_with( ? && object, ? && function ){
    auto ? [ x1, x2, x3 ] = ?( object );
    return ?( (?(function))( ?(x1), ?(x2), ?(x3) );
}

In this context I have the following smaller questions

(1), What difference does auto [ x1, x2, x3 ] = ... vs auto & [ x1, x2, x3 ] = ... vs auto && [ x1, x2, x3 ] = ... actually have. In this context which should be used?

(2), if object is passed as an rvalue, does it make sence to treatx1, x2 and x3 as rvalues as well? ( for possible types they could have T, T &, T &&, T const &, etc. ). Does something similar hold for lvalues and how would this be implemented?

(3), What would the "correct" typing of function be? In the example I put it as an forwarding reference.

(4) Do any other operations need to be applied?

(5) How do I define the return type without loosing potential references returned by function?

Upvotes: 3

Views: 216

Answers (1)

Sam Coutteau
Sam Coutteau

Reputation: 417

Preface

This answer is made to be consistent with std::apply, such that the elements of a tuple are forwarded in the same way by std::apply and call_with. This seems like the most sensible way of approaching this problem as both function act similar, but use a different mechanism ( tuple-like vs structured binding ).

Solution

template< typename T, typename U >
struct copy_cref { using type = U; };
template< typename T, typename U >
struct copy_cref< T &, U > { using type = U &; };
template< typename T, typename U >
struct copy_cref< T const &, U > { using type = U const &; };
template< typename T, typename U >
struct copy_cref< T &&, U > { using type = U &&; };
template< typename T, typename U >
struct copy_cref< T const &&, U > { using type = U const &&; };

template<typename T,typename U>
using copy_cref_t = typename copy_cref<T,U>::type;

template< typename F, typename T >
constexpr decltype(auto) call_with( F && f, T && t )
{
    auto && [ x1, x2, x3, x4 ] = t;

    return std::invoke( 
        std::forward< F >( f ),
        std::forward< copy_cref_t< decltype( t ), decltype( x1 ) > >( x1 ),
        std::forward< copy_cref_t< decltype( t ), decltype( x2 ) > >( x2 ),
        std::forward< copy_cref_t< decltype( t ), decltype( x3 ) > >( x3 ),
        std::forward< copy_cref_t< decltype( t ), decltype( x4 ) > >( x4 )
    );
}

Explanation

std::apply

If we look at the exposition-only implementation of std::apply

template<class F,class Tuple, std::size_t... I>
constexpr decltype(auto)
    apply-impl(F&& f, Tuple&& t, std::index_sequence<I...>) // exposition only
{
    return INVOKE(std::forward<F>(f), std::get<I>(std::forward<Tuple>(t))...);
}

We see that the elements are passed to INVOKE by using std::get on the forwarded tuple. The declarations of std::get, relevant here, are

template< std::size_t I, class... Types >
std::tuple_element<I, tuple<Types...>>::type& get( tuple<Types...>& t ) noexcept;

template< std::size_t I, class... Types >
std::tuple_element<I, tuple<Types...>>::type&& get( tuple<Types...>&& t ) noexcept;

template< std::size_t I, class... Types >
const std::tuple_element<I, tuple<Types...>>::type& get( const tuple<Types...>& t ) noexcept;

template< std::size_t I, class... Types >
const std::tuple_element<I, tuple<Types...>>::type&& get( const tuple<Types...>&& t ) noexcept;

Observe that const and references are simply transferred from the tuple type to the element type. Unlike std::forward_like, which first removes references and then sets the reference, this uses the reference collapsing rules, T && & -> T &. This is the behaviour implemented by copy_cref.

For the specific questions I had.

(1) I still have not found I satisfactory explanation for the difference. It seems that auto && [...] is the most general version.

(2) The approach that std::apply takes seems to be the safest option. I would assume problems arise when you store lvalue and rvalue references to data stored within the same struct / tuple. The answer is thus "it depends".

(3,4,5) No idea, I simply copied std::apply. See the related resources.

Related Resources

https://en.cppreference.com/w/cpp/utility/apply
https://en.cppreference.com/w/cpp/utility/functional/invoke
https://en.cppreference.com/w/cpp/utility/tuple/get

Do structured bindings and forwarding references mix well?
When to use std::invoke instead of simply calling the invokable?
What is the difference between auto and decltype(auto) when returning from a function?

Upvotes: 1

Related Questions