跟我学C++中级篇——C++17标准后的std::invoke系列

221 篇文章 28 订阅

一、函数调用方式

在学习编程的开始,就接触到了函数和函数的调用。可以这样讲,不会调用函数,那么最基础的语言功能便无法使用了。无论是低级到汇编或者高级到哪个语言,函数仍然是其重要的基础一环。可能随便一个学习过编程的都可会说“调用函数不是特别简单么,直接使用其函数名称就可以了,再填上相关的参数”。更或者高级一些的使用类成员函数,加个类限定符就可以了。
这都是正确的。但是,如果在某些特殊的情况下,想实现对函数的调用,该怎么办?这个特殊是指哪些?
假如有一个场景,需要依赖注入,并在注入后执行相关的函数。那么注入的可能是什么?一个普通的函数指针,一个类成员函数指针,一个lambda表达式,一个std::function或std::mem_fn等等,这都有可能。特别是在模板编程中,这种情况更是无法控制,除非使用SFINAE或者Conecpts进行控制。但如果实际的场景就是需要支持上述任意的类型呢?
有过C#开发经验的知道, 它有一个invoke方法可以调用相关的情况如委托等 。前面反复提及过,高级语言的大趋势是趋向一致的,所以C++也从C++17标准起提供了std::invoke(C++17,c++20)和std::invoke_r(c++23)两个函数模板。

二、std::invoke和std::invoke_r

先看一下定义:

std::invoke, std::invoke_r
 C++ Utilities library Function objects 
Defined in header <functional>
template< class F, class... Args >
std::invoke_result_t<F, Args...>
    invoke( F&& f, Args&&... args ) noexcept(/* see below */);   (1)	(since C++17)(constexpr since C++20)
template< class R, class F, class... Args >
constexpr R
    invoke_r( F&& f, Args&&... args ) noexcept(/* see below */);  (2)	(since C++23)
    
1) Invoke the Callable object f with the parameters args as by INVOKE(std::forward<F>(f), std::forward<Args>(args)...). This overload participates in overload resolution only if std::is_invocable_v<F, Args...> is true.
2) Invoke the Callable object f with the parameters args as by INVOKE<R>(std::forward<F>(f), std::forward<Args>(args)...). This overload participates in overload resolution only if std::is_invocable_r_v<R, F, Args...> is true.
Parameters
f	-	Callable object to be invoked
args	-	arguments to pass to f
Return value
1) The value returned by f.
2) The value returned by f, implicitly converted to R, if R is not (possibly cv-qualified) void. None otherwise.

注意上面的constexpr。
其实很简单,就是调用一个F定义的函数对象,参数由Args…来给出。返回值是std::invoke_result_t<F, Args…>。它的定义有点吓人,但使用起来却比较方便,看下面的例子:

#include <functional>
#include <iostream>
#include <memory>

void ge_func(int num) { std::cout << "call general function!value is:" << num << std::endl; }
struct ExFunc {
  void operator()(int num) { std::cout << "call functor!value is:" << num << std::endl; }
};
struct ExObjFunc {
  ExObjFunc() {}
  void ObjFunc(int num) { std::cout << "call object function!value is:" << num << std::endl; }
};

template <typename F, typename T> void TestInvoke(F &&f, T &&t) { std::invoke(f, std::forward<T>(t)); }

int main() {
  TestInvoke(ge_func, 1);
  TestInvoke(ExFunc(), 2);

  ExObjFunc eof;
  std::invoke(&ExObjFunc::ObjFunc, eof, 3);

  std::function<void(int)> func = std::bind(&ExObjFunc::ObjFunc, eof, std::placeholders::_1);
  TestInvoke(func, 3);

  TestInvoke([&](int num) { std::cout << "call lambda!value is:" << num << std::endl; }, 4);
}

注意,如果没有最新的环境,建议直接只使用std::invoke.
上面的代码非常简单,大家可以把自己的相关的函数对象传递进去试一下就明白了,路子都是一样的,没有什么难点。
但是这里面有一个问题,它的返回值看上去有些奇怪std::invoke_result_t ,有过相关开发经验的一眼就知道他一定是从std::invoke_result增加出来一个type类型实现的,下面的就分析一下这个返回值。

三、std::invoke_result和std::result_of

其实在C++11时就出现了std::result_of,在C++14又出现了std::result_of_t,可是在C++17基本就告诉你,别用那个了,哥给你提供了更好的std::invoke_result和std::invoke_result_t。这两个类型没别的,其实就得得到当前函数的调用结果,看名字也很容易明白。
看一下定义:

template< class >
class result_of; // not defined

template< class F, class... ArgTypes >
class result_of<F(ArgTypes...)>;(1)	(since C++11)(deprecated in C++17)(removed in C++20)
template< class F, class... ArgTypes >
class invoke_result; (2)	(since C++17)

它还有两个辅助类型:

template< class T >
using result_of_t = typename result_of<T>::type;(1)	(since C++14)(deprecated in C++17)(removed in C++20)
template< class F, class... ArgTypes >
using invoke_result_t = typename invoke_result<F, ArgTypes...>::type;(2)	(since C++17)

从上面的定义可以看出来,result_of在C++17就被抛弃了,到了C++20就完全被删除了。也就是说,后面只能用invoke_result,可这代码中要是有它,就苦了编码的了。
下面看一个例子:

#include <functional>
#include <iostream>

int testFunc() {
  std::cout << "test" << std::endl;
  return 0;
}
int testFuncArgs(int a, int b) {
  std::cout << "test a+b=" << a + b << std::endl;
  return a + b;
}

template <typename F, typename... Args> void TestArgs(F &&f, Args... args) {
  // static_assert(std::is_same_v<std::result_of_t<F>, int>);

  static_assert(std::is_same_v<std::invoke_result_t<F()>, F>);
  static_assert(std::is_same_v<std::invoke_result_t<F, Args...>, int>);
}
template <typename F> void Test(F &&f) {
  static_assert(std::is_same_v<std::result_of_t<F()>, int>);

  static_assert(std::is_same_v<std::invoke_result_t<F()>, F>);
  static_assert(std::is_same_v<std::invoke_result_t<F>, int>);
}

int main() {
  Test(testFunc);
  TestArgs(testFuncArgs, 1, 2);
}

如果需要的话还可以std::decay_t, std::decay_t…> args来处理具体的类型。
结果类型在普通的编程里其实真得没什么意义,更多的应用是在模板编程和元编程中,通过结果来处理某些情况,同时利用结果的数据类型来达到某些定义使用。其实从标准的发展可以看到auto,decltype,delval等等都在不断的提供着类似的处理手段。所以重要的是灵活运用而不是拘泥于某种形式,达到目的才是重点。

四、总结

C++的变化是有目共睹的,虽然在前两年特殊情况下导致一些新技术没有被应用到想象的版本中,但在后续版本的迭代中,仍然在不断的增加和完善。C++学习的成本看似有些降低了,但实际上可能增加的成本更多了。这并不矛盾,新标准下编程可能更简单了,但背后的逻辑却更复杂了;新标准更容易理解了,但新老标准的代码混合在一起更难了。
况且,大多数的编程人员仍然使用C++98,C++11的普及都没有想象的多,更别提C++14以后的标准了。不过不要着急,慢慢来,只要坚持学习,就会达到彼岸,毕竟,学习成本比创造成本要低得多。C++新版本好歹也要三年才升级一次。

  • 30
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值