第18章、探讨C++新标准

18.1 新类型

2. std::initializer_list
C++11提供了模板类initializer_list, 可将其用作构造函数的参数,
这在第16章讨论过。 如果类有接受initializer_list作为参数的构造函数,
则初始化列表语法就只能用于该构造函数。 列表中的元素必须是同一种
类型或可转换为同一种类型。 STL容器提供了将initializer_list作为参数
的构造函数:

    vector<int> a1(10);//uninitlized vector with 10 elements
    vector<int> a2{10};//initlizer-list ,a2 has 1 elements set to 10
    vector<int> a3{4,6,1};// 3 elements set to 4,6,1

头文件initializer_list提供了对模板类initializer_list的支持。 这个类包
含成员函数begin( )和end( ), 可用于获悉列表的范围。 除用于构造函数
外, 还可将initializer_list用作常规函数的参数:

#include <iostream>
#include<vector>
#include<initializer_list>

using namespace std;

double sum(initializer_list<double> il);


int main()
{
//    vector<int> a1(10);//uninitlized vector with 10 elements
//    vector<int> a2{10};//initlizer-list ,a2 has 1 elements set to 10
//    vector<int> a3{4,6,1};// 3 elements set to 4,6,1
    
    double total=sum({2.5,3.1,4});//4  converted to 4.0
    
    cout << "Hello World!" << endl;
    return 0;
}

double sum(initializer_list<double> il){
    double tot=0;
    for (auto p=il.begin();p!=il.end();p++) {
        tot+= *p;
    }
    return tot;
}

C++11提供了多种简化声明的功能, 尤其在使用模板时。
1. auto
在这里插入图片描述
在这里插入图片描述
2. decltype
关键字decltype将变量的类型声明为表达式指定的类型。 下面的语
句的含义是, 让y的类型与x相同, 其中x是一个表达式:

decltype (x) y;

下面是几个示例:

    double x;
    int n;
    decltype (x*n) q;//q same type as x*n,i.e. ,double
    decltype (&x) pd;//pd same as &x, i.e. ,double *

这在定义模板时特别有用, 因为只有等到模板被实例化时才能确定
类型:

template< typename T,typename U>
void ef(T t,U u)
{
    decltype (T*U) tu;
    
}

其中tu将为表达式TU的类型, 这里假定定义了运算TU。 例如, 如
果T为char, U为short, 则tu将为int, 这是由整型算术自动执行整型提升
导致的。
decltype的工作原理比auto复杂, 根据使用的表达式, 指定的类型可
以为引用和const。 下面是几个示例:

    int j=3;
    int &k= j;
    const int &n =j;    
    decltype (n) i1;   //i1 type const int &
    decltype (j) i2;   //i2 type int
    decltype ((j)) i3;  //i3 type int &
    decltype (k+1) i4;  //i4 type int 

3. 返回类型后置

C++11新增了一种函数声明语法: 在函数名和参数列表后面( 而不
是前面) 指定返回类型:

    double f1(double ,int);// traditional syntax
    auto f2(double,int)->double;//new syntax,return type is double

就常规函数的可读性而言, 这种新语法好像是倒退, 但让您能够使
用decltype来指定模板函数的返回类型:

template< typename T,typename U>
auto eff(T t,U u) -> decltype (T*U)
{
    
    ... 
}

这里解决的问题是, 在编译器遇到eff的参数列表前, T和U还不在
作用域内, 因此必须在参数列表后使用decltype。 这种新语法使得能够
这样做。
4. 模板别名: using =
对于冗长或复杂的标识符, 如果能够创建其别名将很方便。 以前,
C++为此提供了typedef:

    typedef std::vector<std::string>::iterator itType;

C++11提供了另一种创建别名的语法, 这在第14章讨论过:

    using itType =std::vector<std::string>::iterator;

差别在于, 新语法也可用于模板部分具体化, 但typedef不能:

template <typename T>
    using arrl2=std::array<T,12>;// template for multiple aliases 

上述语句具体化模板array<T, int>( 将参数int设置为12) 。 例如,
对于下述声明:

    std::array<double ,l2> a1;
    std::array<std::string,l2> a2;

可将它们替换为如下声明:

    arrl2<double> a1;
    arrl2<std::string> a2;

18.1.6 作用域内枚举

传统的C++枚举提供了一种创建名称常量的方式, 但其类型检查相
当低级。 另外, 枚举名的作用域为枚举定义所属的作用域, 这意味着如
果在同一个作用域内定义两个枚举, 它们的枚举成员不能同名。 最后,
枚举可能不是可完全移植的, 因为不同的实现可能选择不同的底层类
型。 为解决这些问题, C++11新增了一种枚举。 这种枚举使用class或
struct定义:

enum Old1{yes,no,maybe};// traditional form
enum class New1{ never,sometimes,often,always};//new form
enum struct New2{never,lever,sever };//new form

新枚举要求进行显式限定, 以免发生名称冲突。 因此, 引用特定枚
举时, 需要使用New1::never和New2::never等。

18.1.9 右值引用

传统的C++引用( 现在称为左值引用) 使得标识符关联到左值。 左
值是一个表示数据的表达式( 如变量名或解除引用的指针) , 程序可获
取其地址。 最初, 左值可出现在赋值语句的左边, 但修饰符const的出现
使得可以声明这样的标识符, 即不能给它赋值, 但可获取其地址:

    int n;
    int *pt = new int;
    const int b=101; //can't assign to b,but &b is valid
    int & rn =n;// n identifies datum at address &n
    int & rt= * pt;// *pt identifies datum  at address pt  //*pt 标识地址pt处的数据
    const int & rb =b;// b identifies const  datum at address &b

C++11新增了右值引用( 这在第8章讨论过) , 这是使用&&表示
的。 右值引用可关联到右值, 即可出现在赋值表达式右边, 但不能对其
应用地址运算符的值。 右值包括字面常量( C-风格字符串除外, 它表示
地址) 、 诸如x + y等表达式以及返回值的函数( 条件是该函数返回的不
是引用) :

    int x=10;
    int y=23;
    int && r1=13;
    int && r2=x+y;
    double && r3=std::sqrt(2.0);

注意, r2关联到的是当时计算x + y得到的结果。 也就是说, r2关联
到的是23, 即使以后修改了x或y, 也不会影响到r2。
有趣的是, 将右值关联到右值引用导致该右值被存储到特定的位
置, 且可以获取该位置的地址。 也就是说, 虽然不能将运算符&用于
13, 但可将其用于r1。 通过将数据与特定的地址关联, 使得可以通过右
值引用来访问该数据。
程序清单18.1是一个简短的示例, 演示了上述有关右值引用的要
点。
程序清单18.1 rvref.cpp

//rvref.cpp -- simple uses of rvalue references
#include<iostream>

using namespace std;

inline double f(double tf){return  5.0*(tf-32.0)/9.0;};

int main()
{
    double tc=21.5;
    double && rd1= 7.07;
    double && rd2= 1.8*tc +32;
    double && rd3= f(rd2);
    cout<<" tc value and address :" <<tc<<" , "<<&tc <<endl;
    cout<<" rd1 value and address :" <<rd1<<" , "<<&rd1 <<endl;
    cout<<" rd2 value and address :" <<rd2<<" , "<<&rd2 <<endl;
    cout<<" rd3 value and address :" <<rd3<<" , "<<&rd3 <<endl;

    cin.get();
    return 0;
}

该程序的输出如下:

 tc value and address :21.5 , 00000081724FFC10
 rd1 value and address :7.07 , 00000081724FFC30
 rd2 value and address :70.7 , 00000081724FFC38
 rd3 value and address :21.5 , 00000081724FFC40

引入右值引用的主要目的之一是实现移动语义, 这是本章将讨论的
下一个主题。

18.2 移动语义和右值引用

18.2.2 一个移动示例

下面通过一个示例演示移动语义和右值引用的工作原理。程序清单 18.2定义并使用了Useless类,这个类动态分配内存,并包含常规复制构造函数和移动构造函数,其中移动构造函数使用了移动语义和右值引用。为了演示过程,构造函数和析构函数都比较啰嗦,同时Useless类还使用了一个静态变量来跟踪对象数量。另外,省略了一些重要的方法,比如赋值运算符。

程序清单 18.2 useless.cpp

// an otherwise useless class with move semantics

#include <iostream>
using namespace std;

//interface
class Useless
{
private:
    int n;// number of elements
    char * pc;//pointer to data
    static int ct; //number of objects
    void ShowObject() const;

public:
    Useless();
    explicit Useless (int k);
    Useless(int k,char ch);
    Useless(const Useless &f);// regular copy constructor
    Useless(Useless && f);//move constructor
    ~Useless();
    Useless operator+(const Useless & f) const;
    // need operator=() in copy and move versions
    void ShowData() const;//const成员函数


};

// implementation
int Useless::ct =0;
Useless::Useless(){
    ++ct;
    n=0;
    pc=nullptr;
    cout<<" default constructor called, number of objects:"<< ct<<endl;
   ShowObject();
}

Useless::Useless(int k):n(k)
{
    ++ct;
    cout<<" int constructor called, number of objects:"<< ct<<endl;
    pc=new char [n];
    ShowObject();
}
Useless::Useless(int k,char ch):n(k)
{
    ++ct;
    cout<<" int ,char constructor called, number of objects:"<< ct<<endl;
    pc=new char [n];
    for (int i=0;i<n;i++) {
        pc[i]=ch;
    }
    ShowObject();
}

Useless::Useless(const Useless &f):n(f.n){
    ++ct;
    cout<<" copy constructor const called, number of objects:"<< ct<<endl;
    pc=new char [n];
    for (int i=0;i<n;i++) {
        pc[i]=f.pc[i];
    }
    ShowObject();
}

Useless::Useless( Useless &&f):n(f.n){
    ++ct;
    cout<<" move constructor  called, number of objects:"<< ct<<endl;
    pc=f.pc; // steal address
    f.pc=nullptr;//give old object nothing in return
    f.n=0;

    ShowObject();
}

Useless::~Useless(){

    cout<<"  destructor  called,  objects left:"<< --ct<<endl;
    cout<< "deleted object:\n";

    ShowObject();
    delete [] pc;
}
Useless Useless::operator+(const Useless & f) const
{
    cout<< "Entering operator+() \n";
    Useless temp=Useless(n+f.n);
    for (int i=0;i<n;i++) {
        temp.pc[i]=pc[i];
    }

    for (int i=n;i<temp.n;i++) {
        temp.pc[i]=f.pc[i-n];
    }
    cout<< " temp object :\n";
    cout<< " Leaving operator+()\n";
    return temp;
}
void Useless::ShowObject() const {
    cout<<" Number of elements: "<<n;
    cout<<" Data address : "<<(void *) pc<<endl;
}
void Useless::ShowData() const
{
    if(n==0){
        cout<<" (Object empty)";
    }
    else {
        for (int i=0;i<n;i++) {
            cout<<pc[i];//<<endl
        }
    }
    cout<<endl;
}


// application
int main()
{
    {
        Useless one(10,'x');
        Useless two=one; //called copy constructor
        Useless three(20,'o');
        Useless four(one+three);// calls operator+() ,move constructor
        cout<<"Object one:";
        one.ShowData();
        cout<<"Object two:";
        two.ShowData();
        cout<<"Object three:";
        three.ShowData();
        cout<<"Object four:";
        four.ShowData();
    }

    return 0;
}

其中最重要的是复制构造函数和移动构造函数的定义。首先看复制构造函数(删除了输出语句)

Useless::Useless(const Useless &f):n(f.n){
    ++ct;
   // cout<<" copy constructor const called, number of objects:"<< ct<<endl;
    pc=new char [n];
    for (int i=0;i<n;i++) {
        pc[i]=f.pc[i];
    }
  //  ShowObject();
}

它执行深复制,是下面的语句将使用的构造函数:

Useless two =one;// calls copy constructor

引用f 将指向左值对象one。
接下来看移动构造函数,这里也删除了输出语句:

Useless::Useless( Useless &&f):n(f.n){
    ++ct;
    //cout<<" move constructor  called, number of objects:"<< ct<<endl;
    pc=f.pc; // steal address
    f.pc=nullptr;//give old object nothing in return
    f.n=0;

    //ShowObject();
}

它让pc指向现有的数据, 以获取这些数据的所有权。 此时, pc和
f.pc指向相同的数据, 调用析构函数时这将带来麻烦, 因为程序不能对
同一个地址调用delete [ ]两次。 为避免这种问题, 该构造函数随后将原
来的指针设置为空指针, 因为对空指针执行delete [ ]没有问题。 这种夺
取所有权的方式常被称为窃取( pilfering) 。 上述代码还将原始对象的
元素数设置为零, 这并非必不可少的, 但让这个示例的输出更一致。 注
意, 由于修改了f对象, 这要求不能在参数声明中使用const。

在下面的语句中, 将使用这个构造函数:

seless four(one+three);// calls operator+() ,move constructor

表达式one + three调用Useless::operator+(), 而右值引用f将关联到该
方法返回的临时对象。
下面是在Microsoft Visual C++ 2010中编译时, 该程序的输出:
在这里插入图片描述
在这里插入图片描述
注意到对象two是对象one的副本: 它们显示的数据输出相同, 但显
示的数据地址不同( 006F4B68和006F4BB0) 。 另一方面, 在方法
Useless::operator+()中创建的对象的数据地址与对象four存储的数据地址
相同( 都是006F4C48) , 其中对象four是由移动复制构造函数创建的。
另外, 注意到创建对象four后, 为临时对象调用了析构函数。 之所以知
道这是临时对象, 是因为其元素数和数据地址都是0。
如果使用编译器g++ 4.5.0和标记-std=c++11编译该程序( 但将nullptr
替换为0) , 输出将不同, 这很有趣:
在这里插入图片描述
在这里插入图片描述
注意到没有调用移动构造函数, 且只创建了4个对象。 创建对象four
时, 该编译器没有调用任何构造函数; 相反, 它推断出对象four是
operator+( ) 所做工作的受益人, 因此将operator+( )创建的对象转到four
的名下。 一般而言, 编译器完全可以进行优化, 只要结果与未优化时相
同。 即使您省略该程序中的移动构造函数, 并使用g++进行编译, 结果
也将相同。

18.2.3 移动构造函数解析

虽然使用右值引用可支持移动语义, 但这并不会神奇地发生。 要让
移动语义发生, 需要两个步骤。 首先, 右值引用让编译器知道何时可使用移动语义:
在这里插入图片描述
对象one是左值, 与左值引用匹配, 而表达式one + three是右值, 与右值引用匹配。 因此, 右值引用让编译器使用移动构造函数来初始化对象four。 实现移动语义的第二步是, 编写移动构造函数, 使其提供所需的行为。

总之, 通过提供一个使用左值引用的构造函数和一个使用右值引用
的构造函数, 将初始化分成了两组。 使用左值对象初始化对象时, 将使用复制构造函数, 而使用右值对象初始化对象时, 将使用移动构造函数。 程序员可根据需要赋予这些构造函数不同的行为。

这就带来了一个问题: 在引入右值引用前, 情况是什么样的呢? 如
果没有移动构造函数, 且编译器未能通过优化消除对复制构造函数的需求, 结果将如何呢? 在C++98中, 下面的语句将调用复制构造函数:
在这里插入图片描述
但左值引用不能指向右值。 结果将如何呢? 第8章介绍过, 如果实
参为右值, const引用形参将指向一个临时变量:
在这里插入图片描述
就Useless而言, 形参f将被初始化一个临时对象, 而该临时对象被
初始化为operator+()返回的值。 下面是使用老式编译器进行编译时, 程序清单18.2所示程序( 删除了移动构造函数) 的部分输出:
在这里插入图片描述
首先, 在方法Useless::operator+() 内, 调用构造函数创建了temp,并在01C337C4处给它分配了存储30个元素的空间。 然后, 调用复制构造函数创建了一个临时复制信息( 其地址为01C337E8) , f指向该副本。 接下来, 删除了地址为01C337C4的对象temp。 然后, 新建了对象four, 它使用了01C337C4处刚释放的内存。 接下来, 删除了01C337E8处的临时参数对象。 这表明, 总共创建了三个对象, 但其中的两个被删除。 这些就是移动语义旨在消除的额外工作。

正如g++示例表明的, 机智的编译器可能自动消除额外的复制工作, 但通过使用右值引用, 程序员可指出何时该使用移动语义。

18.2.4 赋值

适用于构造函数的移动语义考虑也适用于赋值运算符。 例如, 下面演示了如何给Useless类编写复制赋值运算符和移动赋值运算符:
在这里插入图片描述
上述复制赋值运算符采用了第12章介绍的常规模式, 而移动赋值运
算符删除目标对象中的原始数据, 并将源对象的所有权转让给目标。 不
能让多个指针指向相同的数据, 这很重要, 因此上述代码将源对象中的
指针设置为空指针。
与移动构造函数一样, 移动赋值运算符的参数也不能是const引用,
因为这个方法修改了源对象。

18.2.5 强制移动

移动构造函数和移动赋值运算符使用右值。 如果要让它们使用左
值, 该如何办呢? 例如, 程序可能分析一个包含候选对象的数组, 选择
其中一个对象供以后使用, 并丢弃数组。 如果可以使用移动构造函数或
移动赋值运算符来保留选定的对象, 那该多好啊。 然而, 假设您试图像
下面这样做:
在这里插入图片描述
由于choices[pick]是左值, 因此上述赋值语句将使用复制赋值运算
符, 而不是移动赋值运算符。 但如果能让choices[pick]看起来像右值,
便将使用移动赋值运算符。 为此, 可使用运算符static_cast<>将对象的
类型强制转换为Useless &&, 但C++11提供了一种更简单的方式—使用
头文件utility中声明的函数std::move( )。 程序清单18.3演示了这种技术,
它在Useless类中添加了啰嗦的赋值运算符, 并让以前啰嗦的构造函数和
析构函数保持沉默。

程序清单18.3 stdmove.cpp

//stdmove.cpp  -- using std::move()

#include <iostream>
#include <utility>

using namespace std;
//interface
class Useless
{
private:
    int n;     //number of elements
    char * pc; //pointer ot data
    static int ct;//number of objects
    void ShowObject() const;
public:
    Useless();
    explicit Useless(int k);
    Useless(int k,char ch);
    Useless(const Useless & f);// regular copy constructor
    Useless(Useless &&f);  //move constructor
    ~Useless();
    Useless operator+(const Useless & f) const;
    Useless & operator =(const Useless & f);//copy assignment
    Useless & operator =(Useless &&f);//move assignment
    void ShowData() const;

};

//implementation
int Useless::ct=0;

Useless::Useless(){
    ++ct;
    n=0;
    pc=nullptr;
}

Useless ::Useless(int k):n(k){
    ++ct;
    pc=new char[n];
}

Useless ::Useless(int k,char ch):n(k){
    ++ct;
    pc=new char[n];
    for (int i=0;i<n;i++) {
        pc[i]=ch;
    }
}

Useless ::Useless(const Useless & f):n(f.n){
    ++ct;
    pc=new char[n];
    for (int i=0;i<n;i++) {
        pc[i]=f.pc[n];
    }
}

Useless ::Useless( Useless && f):n(f.n){
    ++ct;
    pc=f.pc; //steal address 移动地址
    f.pc =nullptr ;// give old object nothing in return
    f.n =0;
}

Useless ::~Useless(){
    delete [] pc;
}

Useless & Useless::operator=(const Useless &f)//copy assignment
{
    std::cout<<" copy assignment operator called:\n";
    if(this==&f)
        return *this;
    delete [] pc;
    n=f.n;
    pc=new char[n];
    for (int i=0;i<n;i++) {
        pc[i]=f.pc[i];
    }

    return *this;
}

Useless &Useless::operator=(Useless &&f) //move assignment
{
    cout<<"move assignment operator called:\n";
    if(this==&f)
        return *this;
    delete [] pc;
    n=f.n;
    pc=f.pc;
    f.n=0;
    f.pc=nullptr;
    return *this;
}

Useless Useless::operator+(const Useless &f) const{
    Useless temp=Useless(n+f.n);
    for (int i=0;i<n;i++) {
        temp.pc[i]=pc[i];
    }
    for (int i=n;i<temp.n;i++) {
        temp.pc[i]=f.pc[i-n];
    }
    return temp;
}

void Useless::ShowObject() const
{
    cout<<" Number of elements: "<<n;
    cout<<" Data address: "<<(void*)pc<<endl;
}

void Useless::ShowData() const
{
    if(n==0)
        cout<<" (object empty)";
    else {
        for (int i=0;i<n;i++) {
            cout<<pc[i];
        }
    }
    cout<<endl;
}
//application
int main()
{
    using std::cout;
    {
        Useless one(10,'x');
        Useless two=one+one;// calls move constructor
        cout<<" object one : ";
        one.ShowData();
        cout<<" object two : ";
        two.ShowData();
        Useless three,four;
        cout<< "three = one\n";
        three=one;//automatic copy assignment
        cout<< " now object three = ";
        three.ShowData();
        cout<<" and object one = ";
        one.ShowData();
        cout<<" four = one +two \n";
        four = one+two; //automatic move assignment
        cout<<" now object four = ";
        four.ShowData();
        cout<<"four = move(one)\n";
        four=std::move(one);//forced move assignment
        cout<<" now object four = ";
        four.ShowData();
        cout<< "and object one = ";
        one.ShowData();
    }
   // return 0;
}

该程序输出如下:
在这里插入图片描述

正如您看到的, 将one赋给three调用了复制赋值运算符, 但将move(one)赋给four调用的是移动赋值运算符。
需要知道的是, 函数std::move( )并非一定会导致移动操作。 例如,假设Chunk是一个包含私有数据的类, 而您编写了如下代码:在这里插入图片描述
表达式std::move(one) 是右值, 因此上述赋值语句将调用Chunk的移动赋值运算符—如果定义了这样的运算符。 但如果Chunk没有定义移动赋值运算符, 编译器将使用复制赋值运算符。 如果也没有定义复制赋值运算符, 将根本不允许上述赋值。

对大多数程序员来说, 右值引用带来的主要好处并非是让他们能够编写使用右值引用的代码, 而是能够使用利用右值引用实现移动语义的库代码。 例如, STL类现在都有复制构造函数、 移动构造函数、 复制赋值运算符和移动赋值运算符。

18.4 Lambda函数

见到术语lambda函数( 也叫lambda表达式, 常简称为lambda) 时,您可能怀疑C++11添加这项新功能旨在帮助编程新手。 看到下面的lambda函数示例后, 您可能坚定了自己的怀疑:
在这里插入图片描述

但lambda函数并不像看起来那么晦涩难懂, 它们提供了一种有用的
服务, 对使用函数谓词的STL算法来说尤其如此。

18.4.1 比较函数指针、 函数符和Lambda函数

来看一个示例, 它使用三种方法给STL算法传递信息: 函数指针、函数符和lambda。 出于方便的考虑, 将这三种形式通称为函数对象, 以免不断地重复“函数指针、 函数符或lambda”。 假设您要生成一个随机整数列表, 并判断其中多少个整数可被3整除, 多个少整数可被13整除。生成这样的列表很简单。 一种方案是, 使用vector存储数字,
并使用STL算法generate( ) 在其中填充随机数:
在这里插入图片描述

函数generate( )接受一个区间( 由前两个参数指定) , 并将每个元素设置为第三个参数返回的值, 而第三个参数是一个不接受任何参数的函数对象。 在上述示例中, 该函数对象是一个指向标准函数rand( )的指针。

通过使用算法count_if( ), 很容易计算出有多少个元素可被3整除。与函数generate( )一样, 前两个参数应指定区间, 而第三个参数应是一个返回true或false的函数对象。 函数count_if( )计算这样的元素数, 即它使得指定的函数对象返回true。 为判断元素能否被3整除, 可使用下面的
函数定义:
在这里插入图片描述
下面复习一下如何使用函数符来完成这个任务。 第16章介绍过, 函
数符是一个类对象, 并非只能像函数名那样使用它, 这要归功于类方法
operator( ) ( )。 就这个示例而言, 函数符的优点之一是, 可使用同一个
函数符来完成这两项计数任务。 下面是一种可能的定义:
在这里插入图片描述
在这里插入图片描述
参数f_mod(3)创建一个对象, 它存储了值3; 而count_if( )使用该对象来调用operator( ) ( ), 并将参数x设置为numbers的一个元素。 要计算有多少个数字可被13( 而不是3) 整除, 只需将第三个参数设置为f_mod(3)。

最后, 来看看使用lambda的情况。 名称lambda来自lambda calculus( λ演算) —一种定义和应用函数的数学系统。 这个系统让您能够使用匿名函数—即无需给函数命名。 在C++11中, 对于接受函数指针或函数符的函数, 可使用匿名函数定义( lambda) 作为其参数。 与前述函数f3( )对应的lambda如下:
在这里插入图片描述

这与f3( )的函数定义很像:
在这里插入图片描述

差别有两个: 使用[]替代了函数名( 这就是匿名的由来) ; 没有声
明返回类型。 返回类型相当于使用decltyp根据返回值推断得到的, 这里
为bool。 如果lambda不包含返回语句, 推断出的返回类型将为void。 就
这个示例而言, 您将以如下方式使用该lambda:
在这里插入图片描述

也就是说, 使用使用整个lambad表达式替换函数指针或函数符构造
函数。
仅当lambad表达式完全由一条返回语句组成时, 自动类型推断才管
用; 否则, 需要使用新增的返回类型后置语法:
在这里插入图片描述

程序清单18.4演示了前面讨论的各个要点。
程序清单18.4 lambda0.cpp

//lambda0.cpp -- using lambda expressions
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#include <ctime>


using namespace std;
const long Size1=39L;
const long Size2=100*Size1;
const long Size3= 100*Size2;
bool f3(int x){return x%3==0;}
bool f13(int x){return x%13==0;}



int main()
{
    vector<int> numbers(Size1);

    srand(time(0));
    generate(numbers.begin(),numbers.end(),rand);

    //using function pointers
    cout<< " Sample size = "<<Size1<<'\n';
    int count3= count_if(numbers.begin(),numbers.end(),f3);
    cout<<" Count of number divisible by 3: "<<count3<<'\n';
    int count13= count_if(numbers.begin(),numbers.end(),f13);
    cout<<" Count of number divisible by 13: "<<count13<<'\n';

    //increase number of numbers
    numbers.resize(Size2);
    generate(numbers.begin(),numbers.end(),rand);
    cout<<" Sample size = "<<Size2<<'\n';

    //using function
    class f_mod{
    private:
        int dv;
    public:
        f_mod(int d=1):dv(d){}
        bool operator()(int x){return x%dv==0;}
    };

     count3= count_if(numbers.begin(),numbers.end(),f_mod(3));
    cout<<" Count of number divisible by 3: "<<count3<<'\n';
     count13= count_if(numbers.begin(),numbers.end(),f_mod(13));
    cout<<" Count of number divisible by 13: "<<count13<<'\n';

    //increase number of numbers again
    numbers.resize(Size3);
    generate(numbers.begin(),numbers.end(),rand);
    cout<<" Sample size = "<<Size3<<'\n';

    //using lambdas
    count3= count_if(numbers.begin(),numbers.end(),[](int x){return x%3==0;});
   cout<<" Count of number divisible by 3: "<<count3<<'\n';
    count13= count_if(numbers.begin(),numbers.end(),[](int x){return x%13==0;});
   cout<<" Count of number divisible by 13: "<<count13<<'\n';

    return 0;
}

输出如下
在这里插入图片描述
lambda有一些额外的功能。 具体地说, lambad可访问作用域内的任何动态变量; 要捕获要使用的变量, 可将其名称放在中括号内。如果只指定了变量名, 如[z], 将按值访问变量; 如果在名称前加上&,如[&count], 将按引用访问变量。 [&]让您能够按引用访问所有动态变量, 而[=]让您能够按值访问所有动态变量。 还可混合使用这两种方式, 例如, [ted, &ed]让您能够按值访问ted以及按引用访问ed, [&, ted]让您能够按值访问ted以及按引用访问其他所有动态变量, [=, &ed]让您能够按引用访问ed以及按值访问其他所有动态变量。 在程序清单18.4
中, 可将下述代码:在这里插入图片描述
[&count13]让lambda能够在其代码中使用count13。 由于count13是按引用捕获的, 因此在lambda对count13所做的任何修改都将影响原始count13。 如果x能被13整除, 则表达式x % 13 == 0将为true, 添加到count13中时, true将被转换为1。 同样, false将被转换为0。 因此,for_each( )将lambda应用于numbers的每个元素后, count13将为能被13整
除的元素数。通过利用这种技术, 可使用一个lambda表达式计算可被3整除的元
素数和可被13整除的元素数:
在这里插入图片描述
在这里, [&]让您能够在lambad表达式中使用所有的自动变量, 包
括count3和count13。
程序清单18.5演示了如何使用这些技术。
程序清单18.5 lambda1.cpp

#include <iostream>
#include<vector>
#include<algorithm>
#include<cmath>
#include<ctime>
using namespace std;
const long Size=390000L;

int main()
{
    vector<int> numbers(Size);

    srand(time(0));
    generate(numbers.begin(),numbers.end(),rand);

    //using function pointers
    cout<< " Sample size = "<<Size<<'\n';

    //using lambdas
    //using lambdas

    int count3= count_if(numbers.begin(),numbers.end(),[](int x){return x%3==0;});
   cout<<" Count of number divisible by 3: "<<count3<<'\n';
   int count13=0;
    for_each(numbers.begin(),numbers.end(),[&count13](int x){count13+= x%13==0;});
   cout<<" Count of number divisible by 13: "<<count13<<'\n';

   //using single lambda
   count3=count13=0;
   for_each(numbers.begin(),numbers.end(),[&](int x){count3 +=x%3==0; count13+= x%13==0;});


   cout<<" Count of number divisible by 3: "<<count3<<'\n';
   cout<<" Count of number divisible by 13: "<<count13<<'\n';
    return 0;
}

程序输出
在这里插入图片描述
输出表明, 该程序使用的两种方法( 两个独立的lambda和单个
lambda) 的结果相同。
在C++中引入lambda的主要目的是, 让您能够将类似于函数的表达
式用作接受函数指针或函数符的函数的参数。 因此, 典型的lambda是测
试表达式或比较表达式, 可编写为一条返回语句。 这使得lambda简洁而
易于理解, 且可自动推断返回类型。 然而, 有创意的C++程序员可能开
发出其他用法。

18.5 包装器

C++提供了多个包装器( wrapper, 也叫适配器[adapter]) 。 这些对
象用于给其他编程接口提供更一致或更合适的接口。 例如, 第16章讨论
了bind1st和bind2ed, 它们让接受两个参数的函数能够与这样的STL算法
匹配, 即它要求将接受一个参数的函数作为参数。 C++11提供了其他的
包装器, 包括模板bind、 men_fn和reference_wrapper以及包装器
function。 其中模板bind可替代bind1st和bind2nd, 但更灵活; 模板
mem_fn让您能够将成员函数作为常规函数进行传递; 模板
reference_wrapper让您能够创建行为像引用但可被复制的对象; 而包装
器function让您能够以统一的方式处理多种类似于函数的形式。

下面更详细地介绍包装器function及其解决的问题。

18.5.1 包装器function及模板的低效性

请看下面的代码行:
在这里插入图片描述
ef是什么呢? 它可以是函数名、 函数指针、 函数对象或有名称的
lambda表达式。 所有这些都是可调用的类型( callable type) 。 鉴于可调
用的类型如此丰富, 这可能导致模板的效率极低。 为明白这一点, 来看
一个简单的案例。
首先, 在头文件中定义一些模板, 如程序清单18.6所示。
程序清单18.6 somedefs.h



#ifndef SOMEDEFS_H
#define SOMEDEFS_H
#include <iostream>

using namespace std;
template<typename T,typename F>
T use_f(T v,F f){
    static int count =0;
    count++;
    cout<< " use_f count = "<<count
        <<" ,& count = "<<&count<<endl;
    return f(v);
}
class Fp{
private:
    double z_;

public:
    Fp(double z=1.0):z_(z){

    }
    double operator()(double p){ return z_*p;}

};
class Fq{
private:
    double z_;
public:
    Fq(double z=1.0):z_(z){

    }
    double operator()(double q){ return z_+q;}
};

#endif // SOMEDEFS_H

模板use_f使用参数f表示调用类型:
在这里插入图片描述

接下来, 程序清单18.7所示的程序调用模板函数use_f( )6次。
程序清单18.7 callable.cpp

//callable.cpp -- callable types and templates
#include <iostream>
#include"somedefs.h"
#include<functional>

double dub(double x){return 2.0*x;}
double square(double x){return x*x;}
int main()
{
    double y=1.21;
    cout<<"Function pointer dub:\n";
    cout<<" "<<use_f(y,dub)<<endl;

    cout<<"Function pointer square:\n";
    cout<<" "<<use_f(y,square)<<endl;

    cout<<"Function object Fp:\n";
    cout<<" "<<use_f(y,Fp(5.0))<<endl;

    cout<<"Function object Fq:\n";
    cout<<" "<<use_f(y,Fq(5.0))<<endl;

    cout<<"Lambda expression 1:\n";
    cout<<" "<<use_f(y,[](double u){return u*u;})<<endl;

    cout<<"Lambda expression 2:\n";
    cout<<" "<<use_f(y,[](double u){return u+u/2.0;})<<endl;
    return 0;
}

在每次调用中, 模板参数T都被设置为类型double。 模板参数F呢?
每次调用时, F都接受一个double值并返回一个double值, 因此在6次
use_of( ) 调用中, 好像F的类型都相同, 因此只会实例化模板一次。 但
正如下面的输出表明的, 这种想法太天真了:
在这里插入图片描述
模板函数use_f( )有一个静态成员count, 可根据它的地址确定模板
实例化了多少次。 有5个不同的地址, 这表明模板use_f( )有5个不同的实
例化。

为了解其中的原因, 请考虑编译器如何判断模板参数F的类型。 首
先, 来看下面的调用:
在这里插入图片描述

其中的dub是一个函数的名称, 该函数接受一个double参数并返回
一个double值。 函数名是指针, 因此参数F的类型为double(*) (double):
一个指向这样的函数的指针, 即它接受一个double参数并返回一个
double值。
下一个调用如下:
在这里插入图片描述

第二个参数的类型也是double(*) (double), 因此该调用使用的use_f(
)实例化与第一个调用相同。
在接下来的两个use_f( )调用中, 第二个参数为对象, F的类型分别
为Fp和Fq, 因为将为这些F值实例化use_f( )模板两次。 最后, 最后两个
调用将F的类型设置为编译器为lambda表达式使用的类型。

18.5.2 修复问题

包装器function让您能够重写上述程序, 使其只使用use_f( )的一个实例而不是5个。 注意到程序清单18.7中的函数指针、 函数对象和lambda表达式有一个相同的地方, 它们都接受一个double参数并返回一个double值。 可以说它们的调用特征标( call signature) 相同。 调用特征标是有返回类型以及用括号括起并用头号分隔的参数类型列表定义的, 因
此, 这六个实例的调用特征标都是double (double)。

模板function是在头文件functional中声明的, 它从调用特征标的角度定义了一个对象, 可用于包装调用特征标相同的函数指针、 函数对象或lambda表达式。 例如, 下面的声明创建一个名为fdci的function对象,它接受一个char参数和一个int参数, 并返回一个double值:
在这里插入图片描述

然后, 可以将接受一个char参数和一个int参数, 并返回一个double值的任何函数指针、 函数对象或lambda表达式赋给它。

在程序清单18.7中, 所有可调用参数的调用特征标都相同: double
(double)。 要修复程序清单18.7以减少实例化次数, 可使用
function<double(double)>创建六个包装器, 用于表示6个函数、 函数符和
lambda。 这样, 在对use_f( )的全部6次调用中, 让F的类型都相同
( function<double(double)>) , 因此只实例化一次。 据此修改后的程序
如程序清单18.8所示。
程序清单18.8 wrapped.cpp

//wrapped.cpp --- using a function wrapper as an argument
#include <iostream>
#include"somedefs.h"//
#include<functional>
using namespace std;

double dub(double x){return 2.0*x;}
double square(double x){return x*x;}

int main()
{

    double y=1.21;
    function<double(double)> ef1=dub;
    function<double(double)> ef2=square;
    function<double(double)> ef3=Fq(10.0);
    function<double(double)> ef4=Fp(10.0);
    function<double(double)> ef5=[](double u){return u*u;} ;
    function<double(double)> ef6=[](double u){return u+u/2.0;};

    cout<<"Function pointer dub:\n";
    cout<<" "<<use_f(y,ef1)<<endl;

    cout<<"Function pointer square:\n";
    cout<<" "<<use_f(y,ef2)<<endl;

    cout<<"Function object Fp:\n";
    cout<<" "<<use_f(y,ef3)<<endl;

    cout<<"Function object Fq:\n";
    cout<<" "<<use_f(y,ef4)<<endl;

    cout<<"Lambda expression 1:\n";
    cout<<" "<<use_f(y,ef5)<<endl;

    cout<<"Lambda expression 2:\n";
    cout<<" "<<use_f(y,ef6)<<endl;

    return 0;
}

程序输出
在这里插入图片描述

从上述输出可知, count的地址都相同, 而count的值表明, use_f( )
被调用了6次。 这表明只有一个实例, 并调用了该实例6次, 这缩小了可
执行代码的规模。

18.5.3 其他方式

下面介绍使用function可完成的其他两项任务。 首先, 在程序清单
18.8中, 不用声明6个function<double (double)>对象, 而只使用一个临时
function<double (double)>对象, 将其用作函数use_f( )的参数:在这里插入图片描述

其次, 程序清单18.8让use_f( )的第二个实参与形参f匹配, 但另一种
方法是让形参f的类型与原始实参匹配。 为此, 可在模板use_f( )的定义
中, 将第二个参数声明为function包装器对象, 如下所示:
在这里插入图片描述
这样函数调用将如下:在这里插入图片描述
参数dub、 Fp(5.0)等本身的类型并不是function<double(double)>, 因
此在use_f后面使用了来指出所需的具体化。 这样, T被设置为
double, 而std::function<T(T)>变成了std::function<double(double)>。

18.6 可变参数模板

可变参数模板( variadic template) 让您能够创建这样的模板函数和模板类, 即可接受可变数量的参数。 这里介绍可变参数模板函数。 例如, 假设要编写一个函数, 它可接受任意数量的参数, 参数的类型只需是cout能够显示的即可, 并将参数显示为用逗号分隔的列表。 请看下面的代码:
在这里插入图片描述
这里的目标是, 定义show_list( ), 让上述代码能够通过编译并生成如下输出:在这里插入图片描述
要创建可变参数模板, 需要理解几个要点:
模板参数包( parameter pack) ;
函数参数包;
展开( unpack) 参数包;
递归。

18.6.1 模板和函数参数包

为理解参数包的工作原理, 首先来看一个简单的模板函数, 它显示一个只有一项的列表:在这里插入图片描述
在上述定义中, 有两个参数列表。 模板参数列表只包含T, 而函数参数列表只包含value。 下面的函数调用将模板参数列表中的T设置为double, 将函数参数列表中的value设置为2.15:
在这里插入图片描述

C++11提供了一个用省略号表示的元运算符( meta-operator) , 让
您能够声明表示模板参数包的标识符, 模板参数包基本上是一个类型列
表。 同样, 它还让您能够声明表示函数参数包的标识符, 而函数参数包
基本上是一个值列表。 其语法如下:
在这里插入图片描述

其中, Args是一个模板参数包, 而args是一个函数参数包。 与其他
参数名一样, 可将这些参数包的名称指定为任何符合C++标识符规则的
名称。 Args和T的差别在于, T与一种类型匹配, 而Args与任意数量( 包
括零) 的类型匹配。 请看下面的函数调用:在这里插入图片描述
更准确地说, 这意味着函数参数包args包含的值列表与模板参数包Args包含的类型列表匹配—无论是类型还是数量。 在上面的示例中,args包含值‘S’、 80、 “sweet”和4.5。
这样, 可变参数模板show_list1( )与下面的函数调用都匹配:在这里插入图片描述
就最后一个函数调用而言, 模板参数包Args包含类型int、 int、 int、int、 const char *和std::string, 而函数参数包args包含值2、 4、 6、
8、 “who do we”和std::string(“appreciate”)。

18.6.2 展开参数包

但函数如何访问这些包的内容呢? 索引功能在这里不适用, 即您不能使用Args[2]来访问包中的第三个类型。 相反, 可将省略号放在函数参数包名的右边, 将参数包展开。 例如, 请看下述有缺陷的代码:
在这里插入图片描述
也就是说, args被替换为三给存储在args中的值。 因此, 表示法args…展开为一个函数参数列表。 不幸的是, 该函数调用与原始函数调用相同, 因此它将使用相同的参数不断调用自己, 导致无限递归( 这存在缺陷) 。

18.6.3 在可变参数模板函数中使用递归

虽然前面的递归让show_list1( )成为有用函数的希望破灭, 但正确使用递归为访问参数包的内容提供了解决方案。 这里的核心理念是, 将函数参数包展开, 对列表中的第一项进行处理, 再将余下的内容传递给递归调用, 以此类推, 直到列表为空。 与常规递归一样, 确保递归将终止很重要。 这里的技巧是将模板头改为如下所示:在这里插入图片描述
对于上述定义, show_list3( )的第一个实参决定了T和value的值, 而
其他实参决定了Args和args的值。 这让函数能够对value进行处理, 如显
示它。 然后, 可递归调用show_list3( ), 并以args…的方式将其他实参传
递给它。 每次递归调用都将显示一个值, 并传递缩短了的列表, 直到列
表为空为止。 程序清单18.9提供了一种实现, 它虽然不完美, 但演示了
这种技巧。
程序清单18.9 variadic1.cpp

#include <iostream>
#include<string>
using namespace std;
//using recursion to unpack a parameter pack
// definition for 0 parameters -- terminatin call

void show_list3(){}

//definition for 1 or more parameters
template<typename T,typename... Args>
void show_list3(T value,Args... args)
{
    cout<<value<< " , ";
    show_list3(args...);
}

int main()
{

    int n=14;
    double x=2.71828;
    string mr=" Mr .String object:";
    show_list3(n,x);
    show_list3(x*x,'1',7,mr);
    return 0;

   // return 0;
}

1. 程序说明
请看下面的函数调用:

show_list3(x*x,'1',7,mr);

第一个实参导致T为double, value为x*x。 其他三种类型( char、 int和std::string) 将放入Args包中, 而其他三个值( ‘!’、 7和mr) 将放入args包中。
接下来, 函数show_list3( )使用cout显示value( 大约为7.38905) 和字符串“, ”。 这完成了显示列表中第一项的工作。
接下来是下面的调用:

show_list3(args...);

考虑到args…的展开作用, 这与如下代码等价:

show_list3('1',7,mr);

前面说过, 列表将每次减少一项。 这次T和value分别为char和‘!’,
而余下的两种类型和两个值分别被包装到Args和args中, 下次递归调用
将处理这些缩小了的包。 最后, 当args为空时, 将调用不接受任何参数
的show_list3( ), 导致处理结束。
程序清单18.9中两个函数调用的输出如下:
在这里插入图片描述
2. 改进
可对show_list3( )做两方面的改进。 当前, 该函数在列表的每项后面显示一个逗号, 但如果能省去最后一项后面的逗号就好了。 为此, 可添加一个处理一项的模板, 并让其行为与通用模板稍有不同:

//definition for 1   parameters
template<typename T >
void show_list3(const T &value )
{
    cout<<value<<'\n';

}

这样, 当args包缩短到只有一项时, 将调用这个版本, 而它打印换行符而不是逗号。 另外, 由于没有递归调用show_list3( ), 它也将终止递归。

另一个可改进的地方是, 当前的版本按值传递一切。 对于这里使用的简单类型来说, 这没问题, 但对于cout可打印的大型类来说, 这样做的效率很低。 在可变参数模板中, 可指定展开模式( pattern) 。 为此,可将下述代码:
在这里插入图片描述
这将对每个函数参数应用模式const &。 这样, 最后分析的参数将不
是std::string mr, 而是const std::string& mr。
程序清单18.10包含这两项修改。
程序清单18.10 variadic2.cpp

#include <iostream>
#include<string>
using namespace std;
//using recursion to unpack a parameter pack

// definition for 0 parameters
void show_list(){}

//definition for 1   parameters
template<typename T >
void show_list(const T &value )
{
    cout<<value<<'\n';

}

//definition for 2 or more parameters
template<typename T,typename... Args>
void show_list(const T& value,const Args... args)
{
    cout<<value<< " , ";
    show_list(args...);
}

int main()
{
//    cout << "Hello World!" << endl;
    int n=14;
    double x=2.71828;
    string mr=" Mr .String object:";
    show_list(n,x);
    show_list(x*x,'1',7,mr);
    return 0;


}

该程序的输出如下:
在这里插入图片描述

来源:C++ primer plus
仅供学习,侵删

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值