可调用对象

A successful book is not made of what is in it, but what is left out of it.
?????????????????????????????? ???— Mark Twain

接触C++ 2.0已经有段时间了,简单总结一下C++中认为是函数的东西,或者说类似于函数的东西,我们从标准的C和C++函数到函数对象和lambda表达式慢慢讲起。

1. 常规函数

C++中定义函数的方式有很多,下面我们举例说明,对于一些耳熟能详的概念就一带而过。

  • 标准C函数
bool greater(int arg1, int arg2) { return arg1 > arg2; }
  • 类成员函数
struct number {
  bool greater(int arg1, int arg2) { return arg1 > arg2; }
};

说明一下,在C++中我一般只使用struct来定义类,而不是使用传统的class,后面我所有的文字都会如此,这纯粹是个人的编程风格,读者只要保持自己的习惯,并一如既往坚持下去就行。

  • 类静态函数
struct number {
  static bool greater(int arg1, int arg2) { return arg1 > arg2; }
};

前面关于普通的函数,类的成员函数以及类的静态方法,相信读者已经耳熟能详了,下面介绍一下C++ 2.0的新式函数定义。

  • C++ 2.0的新式函数
    c++11提供了一种新式函数的写法,在函数的末尾说明函数的返回类型,函数开头使用auto关键字,这一用法主要用于编写函数模板。
// C++ 11
auto greater(int arg1, int arg2) -> bool {    /* 尾置返回类型 */
    return arg1 > arg2;
}

上面的用法并不常用,在c++14以后可以完全忽略返回值类型,而由编译器根据return语句自动推断,这里的牵涉内容比较多,就不详细展开了,简单举两个例子。

// C++ 14
int value = 3;
auto answer()  { return value ; }     /* 返回类型 int */
const auto& answer()  { return value ; }     /* 返回类型 const int& */

// 还可以使用 decltype 关键字
decltype(auto) answer()  { return value ; }

2. 函数指针

函数指针(function pointer)它是一个存放函数地址的变量,可以通过这个变量调用该函数。在c++11之前一般使用typedef关键字去定义函数指针类型,在c++11之后可以使用更具表现力的using来替代。

bool greater(int arg1, int arg2) { return arg1 > arg2; }

// C++11以前
typedef bool (*old_cmp)(int, int);
old_cmp cmp = greater;

// C++11以后
using new_cmp = bool (*)(int, int);
new_cmp  cmp = greater;

3. 仿函数

其实,在C++中一直可以定义和使用像函数一样的对象,它们被称为仿函数(Functor)。本质上,就是一个重载的调用操作符的类(call operator),即定义了operator()的类,可以有任意个数和任意类型的参数。

struct functor { 
    return_type operator()(args...) const { ... }
};

另外,值得提一下的是,根据operator()包含的0个、1个或2个参数,这种Functor分别被称为生成器、一元仿函数或二元仿函数,下面分别举例说明。

  • 生成器
struct increase_generator { 
  int operator()() noexcept { return num++; }
private:
  int num = 0;
};

工作原理非常简单,每次调用increase_generator::operator()时,将成员变量num的值返回,并将num的值增加1。

int main() {
  increase_generator num_generator;
  for (int i = 0; i < 3; ++i) {
    std::cout << num_generator() << std::endl;
  }
}
// output: 0 1 2
  • 一元仿函数
struct cube { 
  constexpr int operator()(const int value) const noexcept { return value * value * value; }
};

顾名思义,这个仿函数对它传递的值做了立方运算,并且这个operator()被声明为const,它的行为类似于数学上的纯函数,即无副作用。这里constexpr的作用,有兴趣的读者可以自行去研究一下。

  • 谓词
    一元仿函数一个常用的用途就是当做谓词(predict),即只有一个参数且返回值为bool类型的仿函数。如下:
struct is_even { 
  constexpr bool operator()(const int value) const noexcept { return (value % 2) == 0; }
};

举个使用的例子

int main() {
  std::vector<int> numbers{1, 2, 3, 4, 5};
  numbers.erase(std::remove_if(std::begin(numbers), std::end(numbers), is_even()), 
                std::end(numbers));

  std::copy(std::begin(numbers), std::end(numbers), std::ostream_iterator<int>{std::cout, " "});
  return 0;
}
// output: 1 3 5 

上面的示例使用了Erase-remove惯用法,结合我们定义的is_even仿函数,实现了对vector中偶数元素的删除。

  • 二元仿函数
struct greater { 
  bool operator()(const auto& v1, const auto& v2) const noexcept { return v1 > v2; }
};

4. lambda

  • 看个例子
    我们把上面实现的仿函数is_even,同样的用lambda去实现,如下
auto is_even = [] (auto item) { return (item % 2) == 0; };

is_even(2);  // 返回true

可以看到,使用lambda的实现更加简短,表现力也更丰富,不过通常lambda表达式使用都会内联实现,即在应用时实现。
注:上面的语法需要支持C++14及以上的编译器才可以编译成功。

  • 语法
[capture list] (param list) -> return_type { lambda body;}

说明

  • [capture list] 捕获列表,用于捕获外层变量
    [] 不捕获任何变量
    [&] 捕获外部作用域中所有变量,并作为引用在匿名函数体中使用
    [=] 捕获外部作用域中所有变量,并拷贝一份在匿名函数体中使用
    [x, &y] x按值捕获, y按引用捕获
    [&, x] x按值捕获. 其它变量按引用捕获
    [=, &y] y按引用捕获. 其它变量按值捕获
    [this] 捕获当前类中的this指针,如果已经使用了&或者=就默认添加此选项
  • (param list) 参数列表
    当匿名函数没有参数时,可以省略(param list)部分
  • -> return_type 返回值类型
    C++14以后可以省略
  • { lambda body;} 函数实现

5. std::function包装函数对象

std::function 是一个可调用对象包装器,是一个类模板,可以容纳上述所有的可调用对象,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟它们的执行。

  • 作用
    对函数指针(包括普通函数,类成员函数,类静态成员函数),仿函数,lambda表达式做类型消除,也就是说可以将这些可调用实体都转换成std::function类型

  • 示例
    定义格式std::function<函数类型>

bool greater_global(int arg1, int arg2) { return arg1 > arg2; }   // 普通函数

struct number {
  bool greater_member(int arg1, int arg2) { return arg1 > arg2; }  // 类成员函数
  static bool greater_static(int arg1, int arg2) { return arg1 > arg2; } // 类静态函数
};

// 仿函数
struct greater_functor { 
  bool operator()(int arg1, int  arg2) const noexcept { return arg1> arg2; }
};

auto greater_lambda = [] (int arg1, int arg2) { return arg1 > arg2; };  // lambda
auto greater_lambda2 = [] (auto arg1, auto arg2) { return arg1 > arg2; };  // 通用 lambda

int main() {
  std::function<bool(int, int)> test_function;
  test_function = greater_global;

  number object;
  test_function = std::bind(&number::greater_member, &object, 
                            std::placeholders::_1, std::placeholders::_2);
  test_function = std::bind(&number::greater_static, 
                            std::placeholders::_1, std::placeholders::_2);

  test_function = greater_lambda ;
  test_function = greater_lambda2;
  test_function = greater_functor();
  
  test_function(3, 2);
}
最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容

  • 重新系统学习下C++;但是还是少了好多知识点;socket;unix;stl;boost等; C++ 教程 | 菜...
    kakukeme阅读 19,840评论 0 50
  • 首先需要了解一个 callable 概念,callable 故名就是可以调用的对象。在 c/c++ 中一共存在以下...
    守拙圆阅读 743评论 0 0
  • 重载函数调用运算符 如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象,因为这样的类同时也能存储状...
    土豆吞噬者阅读 806评论 0 0
  • C++ lambda表达式与函数对象 lambda表达式是C++11中引入的一项新技术,利用lambda表达式可以...
    小白将阅读 85,215评论 15 118
  • 一、std::function介绍 std::function可调用函数对象模版类是一个函数包装器模版,该函数...
    wilkingwang阅读 336评论 0 0