lambda 与 priority_queue 以及 function 以及 bind

lambda是一种可调用对象,它是一个对象,每一个lambda都有本身不一样的类型。ios

年轻时觉得STL和lambda混用时会有一些奇怪现象,好比我没法像这样定义优先队列:c++

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

可是却能够这样用sortexpress

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 :less

#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的类型dom

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

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

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

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

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

因此想要正确初始化这个优先队列,还得在构造函数里再把lambda表达式自己传进去c++11

 

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);

相关文章
相关标签/搜索