3.1、std::invoke
为什么要有std::invoke?
在C++17之前,调用不同类型的可调用对象(普通函数、函数指针、成员函数指针、函数对象、lambda等)需要用不同语法:
- • 普通函数或函数指针用
f(args...)
- • 类成员函数指针用
(obj.*pmf)(args...)
或者(pobj->*pmf)(args...)
- • 访问成员变量指针用
obj.*pmd
或pobj->*pmd
- • 函数对象和lambda用
f(args...)
这导致泛型代码写起来非常繁琐,必须针对不同情况写不同调用代码,代码重复且难维护。
std::invoke
的设计哲学就是统一调用接口,让你用同一种语法调用任何可调用对象,不用关心它们具体是什么类型。它是泛型编程的利器,极大简化了模板代码的复杂度,提高了代码的通用性和可读性。
std::invoke的核心功能和语法
std::invoke
是一个函数模板,定义大致如下:
template<typename Callable, typename... Args>
decltype(auto) invoke(Callable&& f, Args&&... args);
它接受一个可调用对象f
和若干参数args...
,根据f
的类型自动选择正确的调用方式,并返回调用结果。
支持调用的对象类型包括:
- • 普通函数和函数指针
- • 类成员函数指针
- • 类成员变量指针
- • 函数对象(仿函数)
- • Lambda表达式
- •
std::function
对象
调用示例:
struct Foo {
int data = 42;
int add(int x) { return data + x; }
};
Foo foo;
auto res1 = std::invoke(&Foo::add, foo, 8); // 调用成员函数
auto res2 = std::invoke(&Foo::data, foo); // 访问成员变量
auto res3 = std::invoke([](int a, int b){ return a * b; }, 3, 4); // 调用lambda
底层原理简析
std::invoke
的实现依赖于模板元编程和SFINAE技术,通过判断传入的Callable
类型,选择最合适的调用方式。它大致分为三种调用模式:
- 1. 成员函数指针调用
如果Callable
是成员函数指针,且第一个参数是对象或指针,调用方式为(obj.*pmf)(args...)
或(pobj->*pmf)(args...)
。 - 2. 成员变量指针访问
如果Callable
是成员变量指针,且第一个参数是对象或指针,调用方式为obj.*pmd
或pobj->*pmd
。 - 3. 普通函数或函数对象调用
其他情况,直接使用f(args...)
调用。
此外,std::invoke
还支持完美转发参数,保证传入参数的值类别(左值/右值)不被破坏,返回类型也会保持正确的引用或值类型。
深度案例讲解
下面通过一个综合案例,带你深入理解std::invoke
的高级用法和底层细节:
#include <iostream>
#include <functional>
struct Widget {
int value = 10;
int multiply(int x) { return value * x; }
static int static_add(int a, int b) { return a + b; }
};
struct Functor {
int operator()(int x, int y) const { return x - y; }
};
int free_function(int x, int y) {
return x + y;
}
int main() {
Widget w;
// 调用成员函数指针
int res1 = std::invoke(&Widget::multiply, w, 5);
std::cout << "res1 (成员函数): " << res1 << "\n"; // 50
// 调用成员变量指针
int res2 = std::invoke(&Widget::value, w);
std::cout << "res2 (成员变量): " << res2 << "\n"; // 10
// 调用静态成员函数
int res3 = std::invoke(&Widget::static_add, 3, 7);
std::cout << "res3 (静态成员函数): " << res3 << "\n"; // 10
// 调用普通函数
int res4 = std::invoke(free_function, 4, 6);
std::cout << "res4 (普通函数): " << res4 << "\n"; // 10
// 调用函数对象
Functor f;
int res5 = std::invoke(f, 9, 4);
std::cout << "res5 (函数对象): " << res5 << "\n"; // 5
// 调用lambda表达式
auto lambda = [](int a, int b) { return a * b + 1; };
int res6 = std::invoke(lambda, 2, 3);
std::cout << "res6 (lambda): " << res6 << "\n"; // 7
// 通过指针调用成员函数
Widget* pw = &w;
int res7 = std::invoke(&Widget::multiply, pw, 4);
std::cout << "res7 (指针调用成员函数): " << res7 << "\n"; // 40
// 通过智能指针调用成员函数
std::shared_ptr<Widget> spw = std::make_shared<Widget>();
int res8 = std::invoke(&Widget::multiply, spw, 3);
std::cout << "res8 (智能指针调用成员函数): " << res8 << "\n"; // 30
return 0;
}
代码细节解析
- • 成员函数指针调用时,
std::invoke
自动判断第一个参数是对象、指针还是智能指针,内部通过operator->
访问成员函数,极大简化了调用代码。 - • 成员变量指针调用时,
std::invoke
返回成员变量的值,避免了手写obj.*pmd
的繁琐。 - • 静态成员函数和普通函数调用本质相同,直接调用即可。
- • 函数对象和lambda调用时,
std::invoke
相当于调用它们的operator()
。 - • 支持智能指针调用成员函数,
std::invoke
内部使用std::pointer_traits
和operator->
,保证兼容性。 - • 参数完美转发,保证传入参数的值类别不丢失,避免不必要的拷贝。
这个案例涵盖了std::invoke
的绝大多数使用场景,掌握它就能在泛型编程中轻松应对各种可调用对象。
常见错误与误区
- 1. 忘记传递对象实例
调用成员函数指针时,必须传入对象或指针,否则编译器会报错。 - 2. 传递对象时未使用引用或指针
对象传递时,如果是临时对象或右值,可能导致调用失败或副本调用,建议使用引用或智能指针。 - 3. 成员变量指针调用时误用参数
访问成员变量时,只需传入对象,无需额外参数。 - 4. 线程传参时未用std::ref导致std::invoke错误
传递引用参数给线程函数时,必须用std::ref
包裹,避免编译器误判。 - 5. 误用std::invoke与std::function
std::invoke
是调用工具,std::function
是可调用对象的封装器,两者职责不同,不能混淆。
面试中可能出现的问题
- 1. 解释
std::invoke
的设计动机和解决了什么问题。 - 2. 如何用
std::invoke
统一调用成员函数指针和普通函数指针。 - 3.
std::invoke
如何支持智能指针调用成员函数。 - 4.
std::invoke
与std::function
的区别和联系。 - 5. 结合代码写出使用
std::invoke
调用成员变量指针的示例。 - 6. 说明
std::invoke
如何实现完美转发,为什么重要。
总结与独到见解
std::invoke
不仅仅是语法糖,它是C++17对可调用对象调用机制的统一抽象,体现了现代C++设计中“接口统一、泛型优先、完美转发”的哲学。它让泛型代码不再为调用细节分心,专注于业务逻辑本身。
本文首发于【讳疾忌医-note】公众号,未经授权,不得转载。
(加入我的知识星球,免费获取账号,解锁所有文章。)
阅读剩余
版权声明:
作者:讳疾忌医-note
链接:https://www.1217zy.vip/archives/1081
文章版权归作者所有,未经允许请勿转载。
THE END