C++11:function、bind和lambda表达式

function和bind其实在c++11之前,在boost库中就有相应的实现,在c++11才被纳入了标准库的体系,而lamdba是c++11新引入的语法糖。再看这篇文章之前一定会纳闷为什么要把这三个东西放到一起来写,这三个东西的联系是什么,其实funtion、bind和lambda表达式其实就是c++的闭包(closure)。

在以往的c++程序中,回调一般都是用函数指针的方式来做(虚函数其实也算是函数指针),因为函数无法携带状态,这个其实是非常不方便的,有的人可能会说,仿函数可以携带状态,但是每个回调你都要去写一个仿函数吗?这个未免有点太不方便了,所以这种情况下,闭包出现了。

什么是闭包?

我们先来看一下维基百科的解释。

In programming languages, a closure, also lexical closure or function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions. Operationally, a closure is a record storing a function[a] together with an environment.[1] The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.[b] Unlike a plain function, a closure allows the function to access those captured variables through the closure's copies of their values or references, even when the function is invoked outside their scope.

简单来说,闭包就是一个带状态的可执行体。它可以捕获函数需要的变量(值捕获或者引用捕获),然后在合适的时间来执行函数。通俗一点来说,一个函数,加上一些变量的组合,就是闭包。最能表示闭包含义的结构就是仿函数了,也就是重载了operator()的类。比如下面的代码,类MyClosure,值捕获了一个Object,然后引用捕获了string(两个的区别就是是否发生拷贝),然后重载了operator(),让这个类可以像一个函数一样调用,就是我们常说的仿函数,这个就是一个非常完整的闭包的例子。

class Object;
class MyClosure
{
public:
    MyClosure(const std::string& ss, Object obj)
        : str(ss), object(obj) {}

    void operator() ()
    {
        std::cout << str << " " << obj;
    }

private:
    const std::string& str;
    Object object;
};

int main()
{
    std::string str = "1234";
    Object obj;
    MyClosure closure(str, obj);
    // 调用仿函数
    closure();
}

在c++中,闭包(closure)的具体实现就是std::bind和lambda表达式,而function就是用来储存生成的闭包。

lambda表达式

关于lambda表达式,我在这里不想过多介绍了,有兴趣的可以参考我之前写的文章。

冲冲:C++ Lambda表达式的完整介绍164 赞同 · 19 评论文章

std::bind

bind的思想实际上是一种延迟执行的思想,将函数和函数需要使用的参数保存起来,然后在需要的时候再调用。这个时候其实会有两种情况,一种是我现在有所有的参数都知道,那么我直接把所有参数绑定好就可以了,一种是我只知道部分参数,这个时候除了绑定需要的参数以外,还需要使用占位符把一些参数暴露出来。

比如下面这样一个函数,我想计算x的k次方,输入为两个参数,输出结果会放到指定的本地文件中。

void Func(int x, int k); //具体实现省略

如果我预先都知道了x和k,那么我当然可以立即执行,假设这个计算非常耗时,所以我想把他放到线程池里去执行,这个时候,我就可以使用std::bind了。

int x = 123;
int k = 456;
// 线程池接收的任务格式为 void()
std::function<void()> ff = std::bind(Func, x, k);
// 放入线程池执行
threadPool.push(ff);

如果这个时候我只知道k,不知道x,x我需要在执行的时刻才可以拿到,但是我并不想每次调用都输入k,这个时候就可使用std::bind的占位符了。

int k = 123;
std::function<void()> func = std::bind(Func, std::placeholders::_1, k);

// 执行的时刻,我知道了x的值
x = 456;
//直接调用func执行
func(x);

std::bind 和 lambda表达式的一些区别

如果你有去看一下上面的关于lambda表达式的介绍文章,那么你就会知道,lambda底层的实现其实就是仿函数,而std::bind的实现,其实也是仿函数,和lambda不同的一个点是,lamdba的函数体是用户自己写的,而std::bind则是调用其他函数,所以std::bind就会比lambda多一个函数指针的大小。

void Func1(int32_t x)
{
    std::cout << x;
}

void Func2(int32_t x, int64_t y)
{
    std::cout << x << y;
}

int main()
{
    int32_t x;
    int64_t y;
    auto f1 = std::bind(Func1, x);
    auto f2 = std::bind(Func2, x, y);

    auto ff1 = [x]() { std::cout << x; };
    auto ff2 = [x, y]() { std::cout << x << y; };

    std::cout << sizeof(f1) << " " << sizeof(f2) << std::endl;
    std::cout << sizeof(ff1) << " " << sizeof(ff2) << std::endl;
    return 0;
}

// 64位机器输出结果
16 24
4 16
  • std::bind捕获一个int32时,成员变量有一个指针和一个int32,按8字节对齐,大小是16;
  • std::bind捕获一个int32和一个int64时,成员变量有一个指针、一个int32和一个int64,按8字节对齐,大小是24;
  • lambda表达式捕获一个int32时,成员变量只有一个int32,大小是4;
  • lambda表达式捕获一个int64时,成员变量有一个int32和一个int64,按8字节对齐,大小是16;

再稍微拓展一下,如果是按引用捕获呢?

void Func1(int32_t& x)
{
    std::cout << x;
}

void Func2(int32_t& x, int64_t& y)
{
    std::cout << x << y << std::endl;
}

int main()
{
    int32_t x;
    int64_t y;
    // bind中引用捕获需要使用std::ref
    auto f1 = std::bind(Func1, std::ref(x));
    auto f2 = std::bind(Func2, std::ref(x), std::ref(y));

    // lambda表达式引用捕获需要使用&
    auto ff1 = [&x]() { std::cout << x; };
    auto ff2 = [&x, &y]() { std::cout << x << y; };

    std::cout << sizeof(f1) << " " << sizeof(f2) << std::endl;
    std::cout << sizeof(ff1) << " " << sizeof(ff2) << std::endl;
    return 0;
}
// 64位机器输出结果
16 24
8 16

std::ref的实现,其实就是用了一个指针,所以就是一个指针的大小,而lambda捕获,其实就是把成员变量变成引用&,而引用的底层实现本身也是一个指针,所以也是一个指针的大小,所以在lambda捕获时,只有一个int32的引用捕获大小就变成了8。

std::bind和lambda的基本上可以达到一样的效果,在c++11中有两点是lambda表达式是无法做到的,1. std::bind可以处理泛型参数;2. lambda可以移动捕获。但是在c++14之后,lambda可以使用auto做类型推导,并且支持初始化捕获,所以这两个问题也就完全不存在了,并且我看到很多文章都在说,尽量使用lambda表达式来替代std::bind。

std::function

std::function 其实就是一个可调用对象包装器,函数指针、仿函数、bind表达式、lambda表达式都是可调用对象,因此都可以使用std::function来接收。

比如std::function<void(int)> 就可以接收一切输入为int,返回为void的可调用结构。

// 接收lambda表达式
std::function<void(int)> f1 = [](int val) { std::cout << val; };

// 接收仿函数
class Functor
{
public:
    void operator() (int val)
    {
        std::cout << val;
    }
};
Functor obj;
std::function<void(int)> f2 = Func1;

// 接收正常函数
void Func1(int val)
{
    std::cout << val;
}
std::function<void(int)> f3 = Func1;


// 接收std::bind绑定的函数
void Func2(int val1, int val2)
{
    std::cout << val1 << val2;
}
std::function<void(int)> f4 = std::bind(Func2, std::placeholders::_1, 10);

// 接收std::bind绑定成员函数
class MyClass
{
public:
    void Func(int val)
    {
        std::cout << val;
    }
};
MyClass myObj;
std::function<void(int)> f5 = std::bind(&MyClass::Func, &myObj, std::placeholders::_1);

std::function的潜在的内存分配问题

std::function在保存lambda或者bind生成的对象时,如果对象的大小大于两个指针的大小,他就需要new一块内存来保存这个对象,然后保存对象的指针。在一些极致性能场景的情况下,这个可能会对性能造成一定的影响。我看到有大佬指出一个详细的例子,大家可以移步问题[C++11有了lambda后bind还有多大意义?]中看一下雷鹏大佬的回答。

接下来我们我们研究一下std::function的内存分配问题,先来看下function的构造函数。

  template<typename _Res, typename... _ArgTypes>
    template<typename _Functor, typename>
      function<_Res(_ArgTypes...)>::
      function(_Functor __f)
      : _Function_base()
      {
	typedef _Function_handler<_Signature_type, _Functor> _My_handler;

        // 如果传入的函数不为空,那么就调用_M_init_functor进行初始化
	if (_My_handler::_M_not_empty_function(__f))
	  {
	    _My_handler::_M_init_functor(_M_functor, std::move(__f));
	    _M_invoker = &_My_handler::_M_invoke;
	    _M_manager = &_My_handler::_M_manager;
	  }
      }

       // 构造函数调用的_M_init_functor函数,
       // 该函数会根据_Local_storage返回的结果决定调用具体的_M_init_functor来初始化
	static void
	_M_init_functor(_Any_data& __functor, _Functor&& __f)
	{ _M_init_functor(__functor, std::move(__f), _Local_storage()); }

        // 采用placement new的方式,把对象放到__functor._M_access()空间上
	static void
	_M_init_functor(_Any_data& __functor, _Functor&& __f, true_type)
	{ new (__functor._M_access()) _Functor(std::move(__f)); }

        // 重新new一个_Functor对象,把调用对象放到_Functor内,然后把_Functor保存在__functor._M_access()上
	static void
	_M_init_functor(_Any_data& __functor, _Functor&& __f, false_type)
	{ __functor._M_access<_Functor*>() = new _Functor(std::move(__f)); }

上面的有个点还需要继续说明一下,那就是_Local_storage的判断逻辑,我们继续来看_Local_storage的代码。

	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;

上面的代码如果调用对象的size小于等于_M_max_size,那么就返回true,那我们在瞅一瞅_M_max_size的大小。

static const std::size_t _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)();
  };

_M_max_size的大小被定义成一个union,我们知道union的大小取决于其中最大元素的大小,而这里个里面最大的元素就是_Undefined_class::*_M_member_pointer了,意思就是成员函数的函数指针,这个一般就是两个指针的大小,具体为啥是两个,我猜应该是一个是对象指针一个是函数指针吧(如果不对还请大佬评论区指出来),所以,当生成的对象大于两个指针时,就会出现内存分配的问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值