Reputation: 417
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?
object
before the structured binding declarationfunction
function
(5) How do I define the return type without loosing potential references returned by function
?
Upvotes: 3
Views: 216
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