关于获取函数结果类型的各种方法

On the various ways of getting the result type of a function

在必须推断函数调用结果类型的情况下,C++ 似乎更乐于帮助我们,提供(至少据我所知有以下)两种解决方案:

我的问题是,两者之间有什么不同吗?是否存在一个不能被另一个替代的上下文,如果不是,为什么我们需要一个类型特征来做一些语言可以开箱即用的事情?

result_of 在应用于给定类型时会产生 std::invoke 的 return 类型 - 它甚至用于 invoke 声明的 return 类型。但是,invoke 涵盖的场景远不止普通的非成员函数调用。您在 decltype(…) 中显示的表达式只是 result_of 检查的众多表达式之一。

因此,仅当 F 保证为函数对象类型时,您的选项才等效。否则 result_of<…>decltype(…) 不是的情况下可能有效:

struct A {int i;};
decltype(std::declval<int A::*>()(std::declval<A>())) // error
std::result_of_t<int A::*(A)> // = int&&, treated as a member access

Demo。 (请注意,从 C++14 开始,result_of 是 SFINAE 友好的,即对于 decltype,无效类型会导致平滑推导失败。)

找一些不是很有经验的 C++ 程序员,让他们猜猜每个代码片段的作用。

第一个正确的有几个?有多少人答对了第二个?

给这个操作起个名字可以让它更易读。

现在请一些稍微更有经验的 C++ 程序员,让他们在使用和不使用 result_of 的情况下获取函数调用的结果类型。有多少人做对了,需要多长时间?

这已经是非常重要的元编程,将其封装后可以更轻松地使用 Google 进行发现,并且更容易正确处理。

另外,第一个版本短了很多。

所以 result_of 非常有用,而且由于标准库本身多次需要它,所以让程序员也可以使用它是微不足道的。

存在三个不同点。

  1. 最初,std::result_of 不需要对 SFINAE 友好。因此,如果要在上下文中使用它来验证 F 是否可以用 Args... 调用,它会给你一个硬错误,而 decltypestd::declval 只会导致可能预期的替代失败。 N3462 修复了规范,使其对 SFINAE 友好。

    虽然在兼容的 C++14 编译器上,两者都是 SFINAE 友好的。 std::result_of_t<Fn(ArgsTypes...)>实际上是根据后者来定义的。来自 [meta.trans.other]:

    If the expression INVOKE (declval<Fn>(), declval<ArgTypes>()...) is well formed when treated as an unevaluated operand (Clause 5), the member typedef type shall name the type decltype(INVOKE (declval<Fn>(), declval<ArgTypes>()...)); otherwise, there shall be no member type.

  2. 正如在 result_of 的定义语言中明确指出的,类型 Fn 可以是任何 INVOKE-able,而显式调用 std::declval<F>() 仅适用于函数和函数对象。所以如果你有:

    using F = decltype(&X::bar);
    using T1 = std::result_of_t<F(X*)>;                        // ok
    using T2 = decltype(std::declval<F>()(std::declval<X*>()); // not ok
    
  3. 对于某些类型,std::result_of 实际上不起作用,因为指定某些 type-ids 是非法的(参见 ).这些类型正在为 F 使用函数(与 pointers/references 函数相反)或为任何 Args... 使用抽象 类。所以考虑一些看似无害的东西,比如:

    template <class F, class R = std::result_of_t<F()>>
    R call(F& f) { return f(); }
    
    int foo();
    call(foo); // error, unresolved overload etc.
    

    这会导致 SFINAE 失败(至少这不是硬错误),因为我们必须形成类型 int()() - 返回函数的空函数。现在,从技术上讲,我们错误地编写了代码。应该是:

    template <class F, class R = std::result_of_t<F&()>>
    R call_fixed(F& f) { return f(); }
    

    这行得通。但是如果我们在 declval 上犯了同样的错误,我们会没事的:

    template <class F, class R = decltype(std::declval<F>()())>
    R call_declval(F& f) { return f(); }