std::invoke 和 std::apply 有什么区别?

24

它们都是通用的调用函数、成员函数或任何可调用对象的方法。从cppreference上看,我唯一看到的真正区别是,在std::invoke中,函数参数(无论有多少个)被forward到函数中,而在std::apply中,参数作为tuple传递。这真的是唯一的区别吗?为什么要创建一个单独的函数来处理tuple


3
鉴于std::apply可能实现使用了std::invoke,因此这似乎是唯一的区别。 - Some programmer dude
我自己没有使用过它们,但我想在模板中,将参数捆绑到一些元组 typename ArgsTupleT 中会更方便,而不是采用 typename... ArgsT 的可变参数方式并使用模式展开。 - alter_igel
也许 apply 的实现者是标准 ml 的大粉丝,哈哈。 - Chen Li
2个回答

33

这就是唯一的区别吗?为什么他们会创建一个单独的函数来处理元组?

因为你需要两个选项,因为它们执行不同的操作。考虑:

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)

特别考虑invoke(g,tup)apply(f,tup)之间的区别,前者不会展开tuple,而后者会。有时你需要同时使用两个操作,这需要以某种方式表达出来。


你说得对,通常这些操作非常相似。实际上,Matt Calabrese正在编写一个名为Argot的库,它结合了这两个操作,并且你不是通过调用的函数来区分它们,而是通过如何修饰参数来区分它们:

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)

15
你使用std::apply是因为:
1:即使你可以访问std::invoke,实现apply也是一件麻烦的事情。将元组转换为参数包不是一个简单的操作。apply的实现看起来像这样(来自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>>>{});
}

当然,这并不是世界上最难写的代码,但也不是完全微不足道的。特别是如果您不知道 index_sequence 元编程技巧的话。
2: 因为通过解包 tuple 的元素来调用函数相当有用。它存在的基本操作是将一组参数打包起来,传递该组参数,然后使用这些参数调用函数。我们已经可以通过单个参数(通过传递值)来实现这一点,但通过 apply,您可以使用多个参数来实现它。
它还允许您执行元编程技巧,例如在语言之间进行元编程处理。您注册一个带有此类系统的函数,并提供函数的签名(以及函数本身)。该签名用于通过元编程处理数据。
当其他语言调用您的函数时,元编程生成的函数会遍历参数类型列表,并根据这些类型从其他语言中提取值。它将其提取到哪里?一些保存值的数据结构。由于元编程不能(容易地)构建一个struct/class,因此您需要构建一个tuple(事实上,支持这种元编程正是 tuple 存在的80%原因)。
一旦构建了tuple<Params>,您就可以使用 std::apply 调用该函数。您无法使用 invoke 完成这项工作。
3: 您不希望每个人都将参数放入 tuple 中,只是为了能够执行类似于 invoke 的操作。
4: 您需要建立调用一个接受 tuple 的函数和使用apply解包tuple之间的区别。毕竟,如果您编写一个模板函数,该函数对用户指定的参数执行 invoke,那么如果用户刚好提供单个 tuple 作为参数,并且您的 invoke 函数解包它,那就糟糕了。
您可以使用其他方法来区分这些情况,但对于简单情况而言,拥有不同的函数是一种足够的解决方案。如果您正在编写更通用的 apply 风格函数,其中您想要能够解包 tuple 并传递其他参数或将多个元组解包到参数列表中(或这两者的组合),则需要一个特殊的 super_invoke 来处理它。
但是,invoke 是简单需求的简单函数。对于 apply 也是如此。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接