本文试图在作者的知识范围内解释清楚C++中Callable这个概念。通常Callable总是跟函数直接等价,但是同时也包括其他可调用对象,例如std::functionstd::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构造
      • 函数对象和参数的类型必须是MoveConstructibleDestructible,否则是UB
    • 如果函数对象以及所有的参数是CopyConstructible,则std::bind返回的对象是CopyConstructible,否则是MoveConstructible

std::function

std::functionstd::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类型,自动匹配转换,最终将参数传入函数调用执行。