C++可调用Callable类型的总结
[TOC]
自从在使用 std::thread
构造函数过程中遇到了 Callable 类型的概念以来用到了很多关于它的使用.
因此本文把使用/调查结果总结出来. 包括 Callable 的基础概念, 典型的 Callable 类型介绍.
例如函数对象(狭义), 函数指针, lambda 匿名函数, 函数适配器, std::function
仿函数等.
Callable 类型
基础
定义(参考):
可调用(Callable) 类型是可应用 INVOKE 操作(
std::invoke
是在 C++17 里定义的类, 感觉意思就是执行函数操作的模板类.)
要求:
一个
T
类型要满足为 callable 需要以下表达式在不求值语境中良构.INVOKE<R>(f, [std::declval]ArgTypes>()...)
即INVOKE<R>(f, t1, t2, ..., tN)
.其中
f
为T
类型的对象,ArgTypes
为适合的实参类型列表,R
为适合的返回类型.R
为 void 的时可以表示为static_cast<void>(INVOKE(f, t1, t2, ..., tN))
.详细地
若
f
是类T
的成员函数指针: 上面等价于(t1.*f)(t2, ..., tN)
或者t1
是指针时((*t1).*f)(t2, ..., tN)
.若
N == 1
且f
是类T
的数据成员指针:INVOKE(f, t1)
等价于t1.*f
, 或者指针形式(*t1).*f
.均不满足上面的情况表明
f
是一个函数对象(Function Object) :INVOKE(f, t1, t2, ..., tN)
等价于f(t1, t2, ..., tN)
.
同时, 对于成员函数指针和数据成员指针, t1
可以是一个常规指针或一个重载了 operator*
的类的对象, 例如智能指针 std::unique_ptr
或 std::shared_ptr
.
可作为参数的标准库
下列标准库设施接受任何可调用(Callable)类型:
库 | 说明 |
---|---|
function(C++11) | 包装具有指定函数调用签名的任意可复制构造类型的可调用对象 (类模板) |
bind(C++11) | 绑定一或多个实参到函数对象 (函数模板) |
reference_wrapper(C++11) | 可复制构造 (CopyConstructible)且可复制赋值 (CopyAssignable)的引用包装器 (类模板) |
result_of (C++11)(C++20 中移除) invoke_result(C++17) | 推导以一组实参调用一个可调用对象的结果类型 (类模板) |
thread (构造函数) | 构造新的 thread 对象 (std::thread 的公开成员函数) |
call_once(C++11) | 仅调用函数一次, 即使从多个线程调用 (函数模板) |
async(C++11) | 异步运行一个函数(有可能在新线程中执行),并返回保有其结果的 std::future (函数模板) |
packaged_task(C++11) | 打包一个函数, 存储其返回值以进行异步获取 (类模板) |
一些典型的 Callable 类型
函数对象 Function Object
一个重载了括号操作符()
的对象, 也就是可以以f(args)
形式进行函数调用的对象.
1 |
|
我的第一印象是它跟函数指针有什么区别? 就像是个函数执行包装器, 一个对象型的函数指针?
但是函数对象本质上还是一个 class 的具体化 object, 里面是可以附带一些成员变量(可以理解为函数对象的状态(state))的, 这就让函数对象的应用场景比函数指针更广阔. 最典型的便是 STL 里了. C++ 的 STL 中的众多 algorithm, 非常依赖于函数对象处理容器的元素. 想按照 STL 算法里的要求实现其功能要提供一些函数对象作为参数, 即谓词参数(predicate). 例如对于 find_if
算法.
1 | class NoLess{ |
对于普通函数来说, 只要签名一致, 其类型就是相同的, 是类型不安全的. 但是这并不适用于函数对象, 因为函数对象的类型是其类的类型. 这样, 函数对象有自己的类型, 这也意味着函数对象可以用于模板参数, 这对泛型编程有很大提升. 因为函数对象一般用于模板参数, 模板一般会在编译时会做一些优化. 因此函数对象一般快于普通函数. 类也可以在使用的时候动态再产生, 节省成本.
既然是类, 那就有它的限制, 例如要注意, 如同其他所有对象(狭义上的对象, 我感觉内置类型其实也可以被叫对象, 按场景区分吧)一样, 如果 pass-by-value 的化, 对象里的成员变量是被复制进去的, 一旦对象被析构了, 里面的成员变量也是无法保存下来的. 所以可以 pass-by-reference/pointer.
函数指针并不是没有其用处了, 对于 C API 库里的某些函数不支持函数对象还是有用武之地的. 例如 <cstdlib>
里面的排序函数 qsort
只能调用函数指针.
1 | void qsort( void *ptr, size_t count, size_t size,int (*comp)(const void *, const void *) ); |
函数
除了普通的函数, 当然也包括类成员函数.
这里不提及模板函数, 因为模板函数的概念只存在于编译期, 运行期的函数没有模板的概念, 都是经过完全特化过的, 因此与普通函数/类成员函数的概念是一致的.
函数指针
1 |
|
Lambda 匿名函数(调用对象)
好处是就地定义使用, 简洁, 易维护.
基本形式
完整声明:
1 | [capture list] (params list) mutable exception-> return type { function body } |
各项具体含义如下
- capture list: 捕获外部变量列表.
- params list: 形参列表.
- mutable指示符: 用来说用是否可以修改捕获的变量, 因为lambda的() operator() 默认是 const 的.
- exception: 异常设定.
- return type: 返回类型, 允许省略 lambda 表达式的返回值定义.
- function body: 函数体.
捕获形式:
捕获形式 | 说明 |
---|---|
[] |
不捕获任何外部变量 |
[变量名, …] |
默认以值得形式捕获指定的多个外部变量(用逗号分隔), 如果引用捕获, 需要显示声明(使用 & 说明符) |
[this] |
以值的形式捕获 this 指针 |
[=] |
以值的形式捕获所有外部变量 |
[&] |
以引用形式捕获所有外部变量 |
[=, &x] |
变量x以引用形式捕获,其余变量以传值形式捕获 |
[&, x] |
变量x以值的形式捕获,其余变量以引用形式捕获 |
省略其中的某些成分来声明”不完整”的Lambda表达式:
序号 | 格式 |
---|---|
1 | [capture list] (params list) -> return type {function body} |
2 | [capture list] (params list) {function body} |
3 | [capture list] {function body} |
一些关于 lambda 表达式的细节
延迟调用
按值捕获与按引用捕获的区别.1
2
3
4int a = 0;
auto f = [=]{ return a; }; // 按值捕获外部变量
a += 1; // a被修改了
std::cout << f() << std::endl; // 输出依旧为0,如果想要跟着被改变需要使用引用捕获lambda 表达式转换成函数指针
没有捕获变量的 lambda 表达式可以直接转换为函数指针, 而捕获变量的 lambda 表达式则不能转换为函数指针.
1
2
3typedef void(*Ptr)(int*);
Ptr p = [](int* p){delete p;}; // 正确, 没有状态的 lambda (没有捕获)的lambda表达式可以直接转换为函数指针
Ptr p1 = [&](int* p){delete p;}; // 错误, 有状态的 lambda 不能直接转换为函数指针嵌套
1
int m = [](int x) { return [](int y) { return y * 2; }(x)+6; }(5); //16
作为 STL 算法函数谓词参数:
1
2vector<int> myvec{ 3, 2, 5, 7, 3, 2 };
sort(myvec.begin(), myvec.end(), [](int a, int b) -> bool { return a < b; });
C++14 中的 lambda 新特性
lambda 捕捉表达式/右值
1
2
3
4
5
6
7
8
9
10
11
12
13// 利用表达式捕获,可以更灵活地处理作用域内的变量
int x = 4;
auto y = [&r = x, x = x + 1] { r += 2; return x * x; }();
// 此时 x 更新为6,y 为25
// 直接用字面值初始化变量
auto z = [str = "string"]{ return str; }();
// 此时z是const char* 类型,存储字符串 string
//不能复制只能移动的对象,可以用std::move初始化变量
auto myPi = std::make_unique<double>(3.1415);
auto circle_area = [pi = std::move(myPi)](double r) { return *pi * r * r; };
cout << circle_area(1.0) << endl; // 3.1415泛型 lambda 表达式:
1
2
3auto add = [](auto x, auto y) { return x + y; };//推断类型
int x = add(2, 3); // 5
double y = add(2.5, 3.5); // 6.0
函数适配器
将函数对象与其它函数对象, 或者特定的值, 或者特定的函数相互组合的产物. 由于组合特性, 函数适配器可以满足特定的需求, 头文件 <functional>
定义了几种函数适配器:
std::bind(op, args...)
: 将函数对象 op
的参数绑定到特定的值 args
.std::mem_fn(op)
: 将类的成员函数转化为一个函数对象.std::not1(op), std::not2(op)
,std::unary_negate
,std::binary_negate
: 一元取反器和二元取反器.
std::bind
这里的函数对象就包括了上面所有的类型, 当然也包含自己, 因此可以利用 std::bind
封装出很多有意思的功能.
下面的例子来自于分享.
嵌套
1
2
3
4
5
6
7
8
9
10// 定义一个接收一个参数,然后将参数加10再乘以2的函数对象
auto plus10times2 = std::bind(std::multiplies<int>{},
std::bind(std::plus<int>{}, std::placeholders::_1, 10), 2);
cout << plus10times2(4) << endl; // 输出: 28
// 定义3次方函数对象
auto pow3 = std::bind(std::multiplies<int>{},
std::bind(std::multiplies<int>{}, std::placeholders::_1, std::placeholders::_1),
std::placeholders::_1);
cout << pow3(3) << endl; // 输出: 27调用类中的成员函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class Person{
public:
Person(const string& n) : name{ n } {}
void print() const { cout << name << endl; }
void print2(const string& prefix) { cout << prefix << name << endl; }
private:
string name;
};
int main()
{
vector<Person> p{ Person{"Tick"}, Person{"Trick"} };
// 调用成员函数print
std::for_each(p.begin(), p.end(), std::bind(&Person::print, std::placeholders::_1));
// 此处的std::placeholders::_1表示要调用的Person对象,所以相当于调用arg1.print()
// 输出: Tick Trick
std::for_each(p.begin(), p.end(), std::bind(&Person::print2, std::placeholders::_1,
"Person: "));
// 此处的std::placeholders::_1表示要调用的Person对象,所以相当于调用arg1.print2("Person: ")
// 输出: Person: Tick Person: Trick
return 0;
}调用 lambda 表达式
1
2
3
4
5vector<int> data{ 1, 2, 3, 4 };
auto func = std::bind([](const vector<int>& data) { cout << data.size() << endl; },
std::move(data));
func(); // 4
cout << data.size() << endl; // 0调用范围内函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20char myToupper(char c){
if (c >= 'a' && c <= 'z')
return static_cast<char>(c - 'a' + 'A');
return c;
}
int main()
{
string s{ "Internationalization" };
string sub{ "Nation" };
auto pos = std::search(s.begin(), s.end(), sub.begin(), sub.end(),
std::bind(std::equal_to<char>{},
std::bind(myToupper, std::placeholders::_1),
std::bind(myToupper, std::placeholders::_2)));
if (pos != s.end()){
cout << sub << " is part of " << s << endl;
}
// 输出: Nation is part of Internationalization
return 0;
}默认 pass-by-value, 如果想要 pass-by-reference, 需要用
std::ref
和std::cref
包装.
std::cref
比std::ref
增加const
属性.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23void f(int& n1, int& n2, const int& n3){
cout << "In function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
++n1;
++n2;
// ++n3; //无法编译
}
int main()
{
int n1 = 1, n2 = 2, n3 = 3;
auto boundf = std::bind(f, n1, std::ref(n2), std::cref(n3));
n1 = 10;
n2 = 11;
n3 = 12;
cout << "Before function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
boundf();
cout << "After function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
// Before function : 10 11 12
// In function : 1 11 12
// After function : 10 12 12
return 0;
}
std::mem_fn
与 std::bind
相比, std::mem_fn
的范围又要小一些, 仅调用成员函数, 并且可以省略掉用于调用对象的占位符.
因此使用 std::men_fn
不需要绑定参数, 可以更方便地调用成员函数.
1 | vector<Person> p{ Person{ "Tick" }, Person{ "Trick" } }; |
std::mem_fn
还可以调用成员变量
1 | class Foo{ |
std::not1
, std::not2
, std::unary_negate
, std::binary_negate
std::not1
, std::not2
分别构造一个与谓词结果相反的一元/二元函数对象.std::unary_negate
, std::binary_negate
分别返回其所保有的一元/二元谓词的逻辑补的包装函数对象, 其对象一般为 std::not1
, std::not2
构造的函数对象,即又加了一层包装.
下面分别是其使用示例:
1 | //std::not1 |
std::not_fn
注意 C++17 已经把上面的 std::not1
, std::not2
, std::unary_negate
和 std::binary_negate
抛弃, 统一由 std::not_fn
替代.
1 | //移除把满足谓词p的元素都copy到容器中 |
std::function
五花八门的 Callable
, 个个都是人才, 但是不好带(不好实现 generic programming), 所以一个把所有 callable 对象封装成统一形式的类型模板.std::function
的实例可以对任何可以调用的目标实体进行存储, 复制, 和调用操作, 实现一种类型安全的包裹.
基础介绍
原型为:
1 | template< class R, class... Args > //R是返回值类型,Args是函数的参数类型 |
其存储的可调用对象被称为 std::function
的目标. 若 std::function
不含目标, 则称它为空. 调用空 std::function
的目标导致抛出 std::bad_function_call
异常.std::function
满足可复制构造 (Copy Constructible) 和可复制赋值 (Copy Assignable) (参考).
瑞士军刀一般的功能, 代码例子如下:
1 |
|
可能的输出
1 | -9 |
回调函数
std::function
的应用之一: 结合 typedef
定义函数类型构造回调函数.
1 | typedef std::function<void(std::string)> CallBack; |
C++可调用Callable类型的总结
https://www.chuxin911.com/C++_callable_objects_summary_20211120/