内容概要

  • 可调用对象概念
  • 可调用对象的类型
  • C++11 std::bind函数
  • C++11 function类型

概念

Object that can appear as the left-hand operand of the call operator. Pointers to functions, lambdas, and objects of a class that defines an overloaded function call operator are all callable objects.

—-《C++ Primer 5th》

书中给出的是描述性定义。

“可调用”一词最容易让人联想到的是“函数”,在我的理解中,函数是抽象出来的有意义的代码块,C++提供了多种封装代码块的能力,这些能力生成的各种对象就是可调用对象。

类型

C++ has several kinds of callable objects: functions and pointers to functions, lambdas, objects created by bind , and classes that overload the function-call operator.

—-《C++ Primer 5th》

简而言之:函数(指针),lambda函数,std::bind生成的函数,重载了()的类的实体对象,bind函数后面再讨论

在STL算法中,不少算法可以让调用者选择自己的断言函数,那个断言函数就是一个符合某种特定要求的可调用对象,模板和可调用对象的合作使得算法函数可以被生成、被定制

// 这段代码要求传入一个可调用对象,它可以接收(string, int)类型的参数
template<typename binaryObj>
void foo(std::string &name, int age, binaryObj obj) {
    obj(name, age);
}

根据上面的分类,我们依次定义三种可调用对象

// 普通的函数
void print_name_age(std::string &name, int age) {
    std::cout << "Common function: " << name << " " 
        << age << std::endl;
}

// 重载了()的类
class Print {
public:
    void operator()(int i) {
        std::cout << "Class override (): " << i << std::endl;
    }
    
    void operator()(std::string &name, int age) {
        std::cout << "Class override (): " << name << " "
            << age << std::endl;
    }
};

int main() {
    std::string name = "Sparrow";
    int age = 21;
    
    // 重载了()的类的实例
    Print p;
    foo(name, age, p);
    
    // 普通函数对象
    foo(name, age, print_name_age);
    
    // lambda函数
    foo(name, age, 
            [](std::string &name, int age) {
                std::cout << "lambda: " << name << " " << age << std::endl;
            });
}

输出:
Class override (): Sparrow 21
Common function: Sparrow 21
lambda: Sparrow 21

简单地说下这些东西存在的意义,函数不用多说;类通过重载就可以当做函数来调用,是很重要的基石——因为类可以有自己的成员变量(lambda函数捕捉列表的本质)、类对象可被当作形参和返回值(函数式编程);lambda函数的本质是匿名函数,初衷是方便程序员写代码,而如今可以在函数内部写出 auto func = lambda expression 的语句,相当于在函数中生成函数(函数式编程)

std::bind

bind函数提高了代码的复用,通过定制已有可调用对象的参数为某些固定值,生成新的可调用对象

#include <iostream>
#include <string>
#include <functional>

// 使用bind函数经常要使用占位符,_1,_2 ... _n
using namespace std::placeholders;

// 此处用普通函数作为可调用对象
void manyParams(int a, int b, std::string c) {
    std::cout << "a: " << a << " "
        << "b: " << b << " "
        << "c: " << c << std::endl;
}

int main() {
    // 调用p1(X)  --> manyParams(X, 0, "fix")
    auto p1 = std::bind(manyParams, ::_1, 0, "fix");
    p1(100);
    
    // 调用p2(X, Y) --> manyParams(Y, X, "fix")
    auto p2 = std::bind(manyParams, ::_2, ::_1, "fix");
    p2(1, 2);
}

输出:
a: 100 b: 0 c: fix
a: 2 b: 1 c: fix

不难猜想bind的实现原理是生成一个函数类,用成员变量记录我们给定的参数,通过包装原有函数再暴露一个最后的调用接口。既然类的成员变量可以是引用类型,那么bind函数的固定形参按理也可以绑定一个变量,这就涉及到std::ref函数。同理lambda函数的引用捕捉,因此可以说类实例是可调用对象这项技术是基石。

function类型

function类型是为了统一所有可调用对象的类型,术语“call signature”指调用对象的返回类型和形参类型特征,通过指明这个特征我们可以用funcion类型来表示类,普通函数,lambda函数

// 表示一个返回值为空,参数类型分别为int和string的可调用类型
std::function<void (int, std::string)>

很多技术可以得益于此,比如说构造函数表、生成函数等等

#include <iostream>
#include <string>
#include <functional>

// 生成函数
std::function<void (std::string&)>
generateFunc(int age) {
    auto func = [age](std::string &name){
        std::cout << "generate function: " <<
            name << " " << age << std::endl;
    };
    return func;
}

int main() {
    std::string name = "Sparrow";
    auto func21 = generateFunc(21);
    func21(name);
}

输出:
generate function: Sparrow 21




Zhu

有问题欢迎发邮件交流