It’s been quite a while since my last blog post. I’m still doing various things with C++, but haven’t had anything worth blogging about. Recently I began doing some testing with std::tuple to see what compilers might be able to optimize away and what they cannot. One of the first things I wanted to do was use it to store parameters – either for object construction or for use on a callable.

EDIT: A comment by FOONATHAN on this entry has a much tighter implementation of the code which can be found here: http://ideone.com/lUbOFA

The only small thing missing is that the tuple should ideally be forwarded inside apply. Other than that it is much cleaner, but the current VS2015 CTP seems to have a compiler bug and it fails to deduce one of the template parameters for this code.

The Mandate

Write some code to expand a std::tuple into the argument list for any callable, using forwarding mechanics to ensure the rvalue bindings are preferred when the tuple itself is an rvalue (or moved) so that move-only contents of the tuple are also supported.

The Code

// // CODE FOR EXPANDING TUPLES INTO A CALLABLE // #if _MSC_VER == 1900 // hack for VS2015 CTP 5 broken decltype(auto) deduction in tuple_into_callable_n #define TUPLE_FWD_RETURN(x) std::conditional_t< std::is_rvalue_reference<decltype(x)>::value, std::remove_reference_t<decltype(x)>, decltype(x)>(x) #else #define TUPLE_FWD_RETURN(x) x #endif // support for expanding tuples into an overloaded function call's arguments #define wrap_overload(func) [](auto&&... ps){ return func( std::forward<decltype(ps)>(ps)... ); } namespace detail { // // base case for building up arguments for the function call // template< typename CALLABLE, typename TUPLE, int INDEX > struct tuple_into_callable_n { template< typename... Vs > static auto apply(CALLABLE f, TUPLE t, Vs&&... args) -> decltype(auto) { return tuple_into_callable_n<CALLABLE, TUPLE, INDEX - 1>::apply( f, std::forward<decltype(t)>(t), std::get<INDEX - 1>(std::forward<decltype(t)>(t)), std::forward<Vs>(args)... ); } }; // // terminal case - do the actual function call // template< typename CALLABLE, typename TUPLE > struct tuple_into_callable_n< CALLABLE, TUPLE, 0 > { template< typename... Vs > static auto apply(CALLABLE f, TUPLE t, Vs&&... args) -> decltype(auto) { return TUPLE_FWD_RETURN(f(std::forward<Vs>(args)...)); }; }; } template< typename FUNC, typename TUPLE > auto tuple_into_callable(FUNC f, TUPLE&& t) -> decltype(auto) { return detail::tuple_into_callable_n< FUNC, decltype(t), std::tuple_size< std::remove_reference_t<TUPLE> >::value >::apply(f, std::forward<decltype(t)>(t) ); }

So basically the method is fairly straight forward: a helper function recursively calls itself, adding on each successive std::get for each ‘layer’, until it reaches index 0 for the tuple – at which point it can pass the variadic arguments it has built up on to the function we want to unpack the tuple into. Since the helper function will need to have a partial specialization for the terminal case when the tuple index is 0 (and the desired function is finally called), it needs to be wrapped as a function object so the struct can be specialized.

In the situation where tuple_into_callable is called with a function as the first parameter (as opposed to a lambda, for example) there is a case where the function could be overloaded. In the case of an overload it would ordinarily be up to the caller to specify which overload is intended. We want the compiler to choose which overload is called based on the tuple’s contained types. So the solution is a handy macro called wrap_overload() which simply turns an overloaded function signature into a generic lambda which is not overloaded. This way the tuple_into_callable is able to have it’s template parameter deduced (theres only one to choose from so the caller does not need to specify), and the whole overload selection is delayed until the actual call is made.

Currently under Visual Studio 2015’s latest CTP there is a bug where a function returning decltype(auto) deduces an incorrect return type in the case where it’s return type is that of a function being passed in as a parameter. The TUPLE_FWD_RETURN macro is a little hack to get around that. (For those interested, the return type deduced is a && which causes the object being returned to go out of scope too early causing an incorrect call to the destructor before the object is used. Correct behavior would be the deduction of a plain value type (non-reference) so that either RVO or move construction can bubble it up.)

Testing The Code

#include <iostream> #include <tuple> #include <string> #include <utility> #include <type_traits> // TODO: INSERT THE TUPLE EXPANSION CODE LISTED ABOVE HERE // // TESTING TYPES & FUNCTIONS // using namespace std; class MoveOnly { public: MoveOnly(const MoveOnly&) = delete; MoveOnly(MoveOnly&& m) {}; static MoveOnly Create() { return MoveOnly(); } private: MoveOnly() {} }; string two_params(int i, int j) { return "two_params int overload called."; } string two_params(float const& i, float const& j) { return "two_params float const& overload called."; } string two_params(float&& i, float&& j) { return "two_params float&& overload called."; } string move_only_receiver(MoveOnly&& m) { return "move_only_receiver called."; } string no_params() { return "no_params called."; } int main() { // lvalue tuple auto t = make_tuple(1, 2); cout << tuple_into_callable(wrap_overload(two_params), t) << endl; // rvalue tuple cout << tuple_into_callable(wrap_overload(two_params), make_tuple(1.0f, 2.0f)) << endl; // const tuple auto const ct = make_tuple(1.0f, 2.0f); cout << tuple_into_callable(wrap_overload(two_params), ct) << endl; // empty tuple -> empty function auto et = make_tuple(); cout << tuple_into_callable(no_params, et) << endl; // tuple with move-only type auto move_only = MoveOnly::Create(); auto mt = make_tuple(move(move_only)); cout << tuple_into_callable(move_only_receiver, move(mt)) << endl; // note: tuple must be move'd into the callable so MoveOnly can be treated as && return 0; }

I think the test code kind of speaks for itself. It tests that the correct overload for a target function is called, tests having rvalue and lvalue tuples, and also tests being able to work with a tuple containing a move-only type. The only thing to remember is that if the tuple contains a move-only type, then it needs to be moved into the tuple_into_callable function.