23

They are both used as a generic method of calling functions, member functions and generally anything that is callable. From cppreference the only real difference I see is that in std::invoke the function parameters (however many they are) are forwarded to the function, whereas in std::apply the parameters are passed as a tuple. Is this really the only difference? Why would they create a separate function just to handle tuples?

KeyC0de
  • 4,728
  • 8
  • 44
  • 68
  • 3
    Considering that [the possible implementation](https://en.cppreference.com/w/cpp/utility/apply#Possible_implementation) of [`std::apply`](https://en.cppreference.com/w/cpp/utility/apply) uses [`std::invoke`](https://en.cppreference.com/w/cpp/utility/functional/invoke) that does indeed seem to be the only difference. – Some programmer dude Sep 21 '18 at 18:21
  • I haven't used them myself but I imagine in templates, it would be convenient to bundle arguments into a some tuple `typename ArgsTupleT`, instead of going the variadic way of `typename... ArgsT` and having to use pattern expansions – alter_igel Sep 21 '18 at 18:22
  • Maybe the implementer of apply is a big fan of standard ml, haha – Chen Li Nov 06 '18 at 06:41

2 Answers2

32

Is this really the only difference? Why would they create a separate function just to handle tuples?

Because you really need both options, since they do different things. Consider:

int f(int, int);
int g(tuple<int, int>);

tuple<int, int> tup(1, 2);

invoke(f, 1, 2); // calls f(1, 2)
invoke(g, tup);  // calls g(tup)
apply(f, tup);   // also calls f(1, 2)

Consider especially the difference between invoke(g, tup), which does not unpack the tuple, and apply(f, tup), which does. You sometimes need both, that needs to be expressed somehow.


You're right that generally these are very closely related operations. Indeed, Matt Calabrese is writing a library named Argot that combines both operations and you differentiate them not by the function you call but rather by how you decorate the arguments:

call(f, 1, 2);         // f(1,2)
call(g, tup);          // g(tup)
call(f, unpack(tup));  // f(1, 2), similar to python's f(*tup)
Barry
  • 286,269
  • 29
  • 621
  • 977
14

You use std::apply because:

1: Implementing apply, even if you have access to std::invoke is a big pain. Turning a tuple into a parameter pack is not a trivial operation. apply's implementation would look something like this (from cppref):

namespace detail {
template <class F, class Tuple, std::size_t... I>
constexpr decltype(auto) apply_impl(F&& f, Tuple&& t, std::index_sequence<I...>)
{
    return std::invoke(std::forward<F>(f), std::get<I>(std::forward<Tuple>(t))...);
}
}  // namespace detail

template <class F, class Tuple>
constexpr decltype(auto) apply(F&& f, Tuple&& t)
{
    return detail::apply_impl(
        std::forward<F>(f), std::forward<Tuple>(t),
        std::make_index_sequence<std::tuple_size_v<std::remove_reference_t<Tuple>>>{});
}

Sure, it's not the hardest code in the world to write, but it's not exactly trivial either. Especially if you don't the index_sequence metaprogramming trick.

2: Because calling a function by unpacking the elements of a tuple is rather useful. The basic operation it exists to support is the ability to package up a set of arguments, pass that set around, and then call a function with those parameters. We already technically have the ability to do that with a single parameter (by passing the value around), but through apply, you gain the ability to do it with multiple parameters.

It also allows you to do metaprogramming tricks, like meta-programmatically doing marshalling between languages. You register a function with such a system, which is given the function's signature (and the function itself). That signature is used to marshal the data through metaprogramming.

When the other language calls your function, the metaprogram-generated function walks the list of parameter types and extracts value(s) from the other language based on those types. What does it extract them into? Some kind of data structure that holds the values. And since metaprogramming cannot (easily) build a struct/class, you instead build a tuple (indeed, supporting metaprogramming like this is 80% of why tuple exists).

Once the tuple<Params> is built, you use std::apply to call the function. You can't really do that with invoke.

3: You don't want to make everyone stick parameters into a tuple just to be able to perform the equivalent of invoke.

4: You need to establish the difference between invokeing a function that takes a tuple, and applying to unpack the tuple. After all, if you're writing a template function that performs invoke on parameters the user specifies, it'd be terrible if the user just so happened to provide a single tuple as an argument and your invoke function unpacked it.

You could use other means to differentiate the cases, but having different functions is an adequate solution for the simple case. If you were writing a more generalized apply-style function, where you want to be able to unpack tuples in addition to passing other arguments or unpack multiple tuples into the argument lists (or a combination of these), you would want to have a special super_invoke that could handle that.

But invoke is a simple function for simple needs. The same goes for apply.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982