lambda 与 priority_queue 以及 function 以及 bind


lambda是一种可调用对象,它是一个对象,每个lambda都有自己不同的类型。

年轻时以为STL和lambda混用时会有一些奇怪现象,比如我无法像这样定义优先队列:

priority_queue<int, vector<int>, [](int a, int b) {return a > b;}> que;

但是却可以这样用sort

sort (vec.begin(), vec.end(), [](int a, int b) {return a < b;});

 以及可以这样用sort

bool less(int a, int b) { return a < b;}

typedef bool (*cmp) (int, int);
cmp mycmp = less;
sort (vec.begin(), vec.end(), mycmp);

sort (vec.begin(), vec.end(), less);  

 

之所以会出现这样的疑问,是因为没有搞清楚函数对象 (也叫可调用对象) 和 模板的类型参数之间的关系, 首先说明如何正确的使用 lambda 对象来实例化priority_queue :

#include <iostream>
#include <queue>
#include <vector>
#include <utility>
 
using my_pair_t = std::pair<size_t,bool>;
 
using my_container_t = std::vector<my_pair_t>;
 
int main()
{
    auto my_comp =
        [](const my_pair_t& e1, const my_pair_t& e2) 
        { return e1.first > e2.first; };
    std::priority_queue<my_pair_t,
                        my_container_t,
                        decltype(my_comp)> queue(my_comp);
    queue.push(std::make_pair(5, true));
    queue.push(std::make_pair(3, false));
    queue.push(std::make_pair(7, true));
    std::cout << std::boolalpha;
    while(!queue.empty()) 
    {
        const auto& p = queue.top();
        std::cout << p.first << " " << p.second << "\n";
        queue.pop();
    }
}

1, 首先这里的my_comp是一个对象,由于每个lambda对象都有一个匿名的类型,所以只能用auto来表达my_comp的类型

2, 优先队列的声明是这样的:

template<
    class T,
    class Container = std::vector<T>,
    class Compare = std::less<typename Container::value_type>
> class priority_queue;

所以要实例化一个优先队列,尖括号里得填typename啊, 由于 lambda 对象的类型是匿名的,所以用decltype搞定,再然后光这样是不行的,这会来看构造函数:

explicit priority_queue( const Compare& compare = Compare(),
                         const Container& cont = Container() );

可以看到,如果我们构造时,不指定特定的compare对象,那么就用typename Compare的默认构造函数构造一个,然而lambda表达式的匿名类型是没有默认构造函数的,

所以想要正确初始化这个优先队列,还得在构造函数里再把lambda表达式本身传进去

 

3, 函数指针,函数, lambda表达式,函数对象之间的关系

首先看sort的一个重载声明:

template <class RandomAccessIterator, class Compare>
  void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp);

这里sort接受一个函数对象,所以直接给传递lambda对象是可以的,因为lambda对象属于函数对象嘛,在C++里凡是能够使用函数调用运算符的,都是函数对象,所以

函数对象有: 函数, 函数指针, 重载了()的类的对象,以及lambda对象。

注意,函数和函数指针是两个不同的东西,就像虽然数组在作为参数时会被当成指针传递,但是他们依旧不是同一个东西,即:

虽然在非引用时,数组和函数都会被转换成指针(包括模板参数推断也会这样),但是他们仍旧不是指针,指针的例子不用多说,举个函数的例子:

template <typename T>
void t(const T& t) {
    cout << std::is_pointer<T>::value << endl;
    t();
}

template <typename T>
void e(T t) {
    cout << std::is_pointer<T>::value << endl;
    t();
}

void f() {
}
 int main() {
    t(f);

    cout << endl;
    e(f);
    return 0;
}

 

第一次调用输出的是false,第二次则是true,这也就是说函数本身 和 函数指针也是两个东西

 

至此也就解释了一开始说到的如何使用sort以及priority_queue的原因了

 

 

 

/*---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------*/

关于function 和 bind :

搞清楚函数对象、函数指针、函数以及lambda对象之间的关系之后,很明显这些函数对象的类型是不一样的,并且在c++11中,函数 的类型可以由函数的调用方式来表示

比如一个函数(函数本身,而不是函数指针或者其他什么东西) int f (int, int) {...},那么我们可以用int(int, int)来表示它的类型,但是值得注意的一点的是,如果想要声明一个

函数,并不能直接

int(int, int) f;

不过我们可以这样:

int a (int, int) {return 1;}
auto b = a;

 

关于这些东西的类型细节,我们可以通过下面的代码来详细的说明:

int a(int, int) {return 1;}
int f(int, int) { return 2; }
int main() {
    auto b = []() { return 1; };
    auto c = b;
    int (*d)(int, int);
    d = a;
    auto e = a;
    cout << boolalpha;
    cout << is_same<int(int, int), decltype(a)>::value << endl;       //true
    cout << is_same <decltype(a), decltype(f)>::value << endl;        //true
    cout << is_same<int(int, int), decltype(b)>::value << endl;       //false
    cout << is_same<int(int, int), decltype(c)>::value << endl;       //false
    cout << is_same<decltype(a), decltype(d)>::value << endl;         //false
    return 0;
}

 

那么问题就来了, 这些可调用对象的类型都不一致,但是他们的调用方式却可以是一致的,比如函数 int a(int); 和 lambda [](int)->int{...}他们都接受一个int类型作为参数,然后

返回一个int类型,可是由于它们的类型不一致,我们无法在函数和lambda对象之间进行拷贝和赋值:

比如我们无法将lambda表达式插入到std::set<int(*)(int)>中。。。

为了解决此类问题,有一群变态(是的,一群变态。。。)他们实现了一个function模板,

function模板是这样一个东西:

function仅仅以函数对象的调用方式来区分类型,也就是说,通过decltype([](int){return 1;})实例化的function对象,和通过int(int) (即上文提到的"函数本身"的类型)实例化的function对象的类型是一致的并且可以相互拷贝的。

具体使用方式可以参考下面的代码:

// function example
#include <iostream>     // std::cout
#include <functional>   // std::function, std::negate

// a function:
int half(int x) {return x/2;}

// a function object class:
struct third_t {
  int operator()(int x) {return x/3;}
};

// a class with data members:
struct MyValue {
  int value;
  int fifth() {return value/5;}
};

int main () {
  std::function<int(int)> fn1 = half;                    // function
  std::function<int(int)> fn2 = &half;                   // function pointer
  std::function<int(int)> fn3 = third_t();               // function object
  std::function<int(int)> fn4 = [](int x){return x/4;};  // lambda expression
  std::function<int(int)> fn5 = std::negate<int>();      // standard function object

  std::cout << "fn1(60): " << fn1(60) << '\n';
  std::cout << "fn2(60): " << fn2(60) << '\n';
  std::cout << "fn3(60): " << fn3(60) << '\n';
  std::cout << "fn4(60): " << fn4(60) << '\n';
  std::cout << "fn5(60): " << fn5(60) << '\n';

  

  fn1 = fn3;                                             
  std::cout << "changed fn1(60): " << fn1(60) << '\n';

// stuff with members:
  std::function<int(MyValue&)> value = &MyValue::value;  // pointer to data member
  std::function<int(MyValue&)> fifth = &MyValue::fifth;  // pointer to member function

  MyValue sixty {60};

  std::cout << "value(sixty): " << value(sixty) << '\n';
  std::cout << "fifth(sixty): " << fifth(sixty) << '\n';

  return 0;
}

 

从上面的代码中可以看到,只要调用方式相同,不管是类的成员指针、函数、函数指针、lambda对象,只要被function包裹之后,他们的类型就都是一致的,并且是可以相互拷贝的 (即使他们包裹的函数对象的具体类型不一致)。但是需要注意的一点是 :因为function的构造函数中,会将函数对象的一份decay copy (即去除了const volatile 以及reference之后的拷贝) 保存到function对象内部,所以如果被包裹的函数对象不能拷贝,那么function将会引发编译错误,比如:

struct foo {
    explicit  foo(int) { p = 1;}
    foo (const foo&) = delete;
  //  foo(const foo&) { static int copyCount = 0; cout << ++copyCount << endl; }; // 会发生三次拷贝
    int operator()(int) {return 1;}
    int p;
};

int main() {
    function<int(int)> fn1 = foo(1);
    function<int(int)> fn2 = foo(2);
    fn1 = fn2;
    return 0;
}

上面的代码会发生编译错误: 引用了deleted 的拷贝构造函数,进一步的,如果把拷贝构造函数换成注释掉的部分,可以看到发生了三次拷贝。

 

 

/*---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------*/

关于bind:

在了解了各种函数对象的细节之后,考虑当可调用对象有默认参数时的情景,如:

int f(int, int arg2 = 0);

此时f 的类型是 int(int, int) (可以验证,此处不再贴代码证明),而f的调用方式却可以是这样的 f(0)

这个时候bind就派上用场了,我们可以通过 bind 将默认参数绑定到某个可调用对象上,从而得到一个全新的,调用方式不一样的可调用对象,bind 是一种给可调用对象加上默认参数

的手法, 但是bind比默认参数更强大, bind可以显式的改变函数对象的 调用方式,以更好的配合 function 和 STL 的组件, bind 不但可以设置默认参数,还能改变参数的位置, bind的

一般形式为:

auto newCallable =  bind (callable, arg_list)

更具体的:

auto newCallable = bind (callable, _2, arg2, _1)

这意味着我们调用 newCallable(1, 2) 时, 等价于 callabe(2, arg2, 1), 并且此时 newCallalbe 的调用方式 也已经变成类似于 int (int, int)这样只接受两个参数的类型了。

上面的_1, _2叫做占位符(placeholders), 属于命名空间 std::placeholders,其中

_1 表示 newCallable的第一个参数, _2 表示 newCallable 的第二个参数,以此类推。

 

值得注意的是:

bind 对于非占位符的传递方式是拷贝!!比如对于无法拷贝的cout流,我们只能这样写bind

 auto newCallable = bind (callable, _2, std::ref(arg2), _1);


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM