C++ 可调用实体
本文试图在作者的知识范围内解释清楚C++中Callable
这个概念。通常Callable
总是跟函数
直接等价,但是同时也包括其他可调用对象,例如std::function
、std::bind
、lamda表达式、成员函数、可调用对象(实现()
操作符的类)等。以下内容函数
特指全局定义的函数。
函数是一种类型
在任何讨论之前,有一点需要明确的是C++中函数
是一种类型type
, 这种类型唯一的定义了一个signature
;类型可以被声明,在该类型下可以有多个函数定义:
例1:
// 类型名称:void(int*);函数名称:func1;signature:void(int*)
void func1(int *);
// 类型名称:type_func; signature: void(int*)
typedef void type_func(int *);
// 类型名称为void(int*)的函数
type_func func2;
// 函数定义
void func1(int *) {}
void func2(int *) {}
函数类型不是一等公民
什么是编程语言中的一等公民
首先可以阅读Wikipedia中的解释。总结下来,就是要全部满足如下几个条件:
- 这个实体可以被赋值
- 这个实体可以做
==
运算 - 这个实体可以作为函数的参数传入
- 这个实体可以作为函数的返回值
函数不可被赋值
在例1中,不可以做如下操作:
type_func func3 = func1;
因为在C++中,只有变量
可以被赋值,func3
, func4
都不是变量,是函数
。
函数可以做等号运算和函数参数
在例1的基础上,如果编译运行如下代码:
int main() {
if (func1 == func2){
std::cout << "func1 is equal to func2" << std::endl;
}else{
std::cout << "func1 is not equal to func2" << std::endl;
}
}
程序能够正常编译运行,输出”func1 is not equal to func2”。但这不是因为函数类型本身做了等号运算,而是函数名称被隐式转换为函数指针类型:
void compare_func(type_func func1, type_func func2) {
if (func1 == func2) {
std::cout << "func1 is equal to func2" << std::endl;
} else {
std::cout << std::boolalpha << std::is_pointer<decltype(func1)>::value
<< std::endl;
std::cout << std::boolalpha << std::is_pointer<decltype(func2)>::value
<< std::endl;
std::cout << "func1 is not equal to func2" << std::endl;
}
}
int main() { compare_func(func1, func2); }
- 函数可以做等号运算,也可以作为函数参数传入;但为隐式转换为函数指针类型
函数不可以返回函数类型
// 错误,不可以直接返回函数类型
type_func ret_func1() { return func1; }
// 正确,可以返回函数指针类型
type_func *ret_func2() { return func2; }
函数指针是一等公民
综上,C++函数类型本身不是一等公民,但是其指针是一等公民;而在函数作为函数参数和等号比较运算时会隐式转换为函数指针,让函数类型具备了一等公民的某些特点。
成员函数是一种特殊的函数
成员函数是一种特殊的函数:
#include <functional>
#include <iostream>
#include <memory>
#include <type_traits>
class SampleClass {
public:
SampleClass(const std::string &spc) : spec{spc} {}
std::string spec;
void print();
};
void SampleClass::print() { std::cout << this->spec << std::endl; }
int main() {
SampleClass myclass{"hello"};
// 指向成员函数的类型,注意括号中包含类的名称
typedef void (SampleClass::*mem_ptr)();
mem_ptr ptr = &SampleClass::print;
// 调用print
(myclass.*ptr)();
// C++17以上的编译器,可以使用如下
std::invoke(ptr, &myclass);
}
函数对象(function object)
如果一个类型实现了()
操作符,则该类型是一个函数对象。函数对象可以像函数一样被调用:
#include <functional>
#include <iostream>
class FunctorClass {
public:
int a{1};
void operator()() { std::cout << a << std::endl; }
};
int main() {
FunctorClass f;
f();
// since C++17
std::invoke(f);
}
std::bind
std::bind接收一个Callable
,返回一个函数对象。Callable
可以是:
- 函数指针
- 类成员函数指针
- 类成员指针(成员指针,虽然没有调用,但是一个
Callable
) - 函数对象
std::bind
是一个包装器,跟std::function
类似。作用是将预定义的参数绑定到对应的Callable
上:
#include <functional>
#include <iostream>
#include <memory>
#include <random>
void f(int n1, int n2, int n3, const int &n4, int n5) {
std::cout << n1 << ' ' << n2 << ' ' << n3 << ' ' << n4 << ' ' << n5 << '\n';
}
int main() {
int n = 7;
// 占位符表示的是f1被实际调用时,用户传入的参数位置
auto f1 = std::bind(f, std::placeholders::_2, 42, std::placeholders::_1,
std::cref(n), n);
f1(1, 2); // 2,42,1,7,7
auto f2 = [](int a, int b) { std::cout << a << b << std::endl; };
// bind一个lamda;lamda表达式也是一个函数对象
auto f3 = std::bind(f2, 1, std::placeholders::_1);
f3(2);
}
需要注意:
std::bind
返回一个匿名类型的函数对象(function object),类型由编译器自动创建std::bind
的所有参数都是Universal Reference,所有的传入参数会根据用户传入的参数进行构造std::bind
返回的对象包含了传入的函数对象和所有参数的实例,这些实例从用户传入的Universal Reference构造- 函数对象和参数的类型必须是
MoveConstructible
和Destructible
,否则是UB
- 函数对象和参数的类型必须是
- 如果函数对象以及所有的参数是
CopyConstructible
,则std::bind返回的对象是CopyConstructible
,否则是MoveConstructible
std::function
std::function与std::bind
一样,也是一个Callable
的包装器。std::function
对象是可拷贝,可赋值的:
#include <functional>
#include <iostream>
void f(int *) { std::cout << "func called" << std::endl; }
class Funtor {
public:
Funtor(const std::string &name) : name{name} {}
std::string name;
std::string operator()() { return this->name; }
std::string get_name() {
std::cout << "get_name called" << std::endl;
return this->name;
}
std::string get_name_2() {
std::cout << "get_name_2 called" << std::endl;
return this->name;
}
};
typedef std::string (Funtor::*get_name)();
int main() {
std::function<void(int *)> funtor = f;
int a;
funtor(&a);
// 指向类成员函数的指针
get_name get = &Funtor::get_name;
Funtor func{"elela"};
(func.*get)();
// std::bind可以bind指向成员函数的Callable
std::bind(get, &func)();
// std::function 不能直接存储一个成员函数指针,需要通过std::bind传递
// std::function<get_name> func_wrapper;
std::function<std::string(void)> func_wrap =
std::bind(&Funtor::get_name_2, &func);
func_wrap();
}
std::function内存分配
在std::function
存储的Callable
较小时(实验测试16字节),std::function存储在栈上,否则会在heap上申请内存:
源码:
// 是否在栈上的判断条件
static const bool __stored_locally =
(__is_location_invariant<_Functor>::value
&& sizeof(_Functor) <= _M_max_size
&& __alignof__(_Functor) <= _M_max_align
&& (_M_max_align % __alignof__(_Functor) == 0));
typedef integral_constant<bool, __stored_locally> _Local_storage;
...
// _M_max_size=sizeof(_Nocopy_types)
union _Nocopy_types
{
void* _M_object;
const void* _M_const_object;
void (*_M_function_pointer)();
void (_Undefined_class::*_M_member_pointer)();
};
...
// 内存分配函数◊
static void
_M_init_functor(_Any_data& __functor, _Functor&& __f)
{ _M_init_functor(__functor, std::move(__f), _Local_storage()); }
...
// 分配判断:如果在栈上则使用placement new,否则使用new
static void
_M_init_functor(_Any_data& __functor, _Functor&& __f, true_type)
{ ::new (__functor._M_access()) _Functor(std::move(__f)); }
static void
_M_init_functor(_Any_data& __functor, _Functor&& __f, false_type)
{ __functor._M_access<_Functor*>() = new _Functor(std::move(__f)); }
lamda expression
闭包也是一个函数对象,类似std::bind
。当lamda表达式以捕获值的形式捕获对象时,lamda表达式的结果的拷贝和移动构造函数由捕获的对象决定,因为捕获的对象将作为匿名闭包类型的成员:
The copy constructor and the move constructor are declared as defaulted and may be implicitly-defined according to the usual rules for copy constructors and move constructors.
可调用对象(Callable)
所以,C++中可调用对象可以是如下类型:
- 函数类型(包括函数指针,函数类型会默认转换为函数指针)
- 类成员函数指针
- 类成员指针
- 函数对象:任意实现了
()
操作符的类型,这一类包括std::bind
std::function
- lamda表达式
Callable就是能被std::invoke调用的对象
在C++ 17及以后,std::invoke
可以直接调用Callable
对象,其会根据传入的Callable
类型,自动匹配转换,最终将参数传入函数调用执行。