Modern C++学习笔记

1 关键字

1.1 constexpr

《Effective Modern C++》Item 15:
Use constexpr whenever possible.

1.1.1 constexpr object

constexpr object是加强版的const object,它表示一个数值不仅是const,而且在编译时就已经知道其结果。适用于要求整型常量表达式的场景,如数组大小,整型的模板参数(如std::array的长度),枚举值,对齐标记等。

int sz; // non-constexpr variable
…
constexpr auto arraySize1 = sz; // error! sz's value not
// known at compilation
std::array data1; // error! same problem
constexpr auto arraySize2 = 10; // fine, 10 is a

1.1.2 constexpr function

constexpr修饰的函数,并不是总是在编译时期确定函数的返回值,它的特点是:

  • 如果传给constexpr函数的参数在编译阶段就可知,那么函数的结果也将在编译阶段计算出来。
  • 如果一个或多个参数在编译阶段是未知的,那么该constexpr函数与普通函数无异,需要在runtime才算出返回结果。这种情况下,该函数不适用于要求整型常量表达式的场景,否则编译器会报错。

下面是两个constexpr函数的例子:

constexpr // pow's a constexpr func
int pow(int base, int exp) noexcept // that never throws
{
    … // impl is below
}
constexpr auto numConds = 5; // # of conditions
std::array results; // results has 3^numConds elements
// return size of an array as a compile-time constant. (The
// array parameter has no name, because we care only about
// the number of elements it contains.)
template // see info
constexpr std::size_t arraySize(T (&)[N]) noexcept 
{ 
   // below on constexpr and noexcept
   return N; 
}

int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; // keyVals has
// 7 elements
int mappedVals[arraySize(keyVals)]; // so does mappedVals
std::array mappedVals2;

constexpr函数要求参数和返回类型都是literal type,literal type是指在编译阶段就能确定的类型。在C++11中,built-in类型(除了void)都属于literal type。自定义类型也可是literal的,因为构造函数和成员函数都可以是constexptr:

class Point {
public:
    constexpr Point(double xVal = 0, double yVal = 0) noexcept
        : x(xVal), y(yVal)
    {}
    constexpr double xValue() const noexcept { return x; }
    constexpr double yValue() const noexcept { return y; }
    void setX(double newX) noexcept { x = newX; }
    void setY(double newY) noexcept { y = newY; }
private:
    double x, y;
};

constexpr Point p1(9.4, 27.7); // fine, "runs" constexpr ctor during compilation
constexpr Point p2(28.8, 5.3); // also fine

constexpr
Point midpoint(const Point& p1, const Point& p2) noexcept
{
    return { (p1.xValue() + p2.xValue()) / 2, // call constexpr
    (p1.yValue() + p2.yValue()) / 2 }; // member funcs
}
constexpr auto mid = midpoint(p1, p2); // init constexpr object w/result of constexpr function

1.2 delete

《Effective Modern C++》Item 11:
Prefer deleted functions to private undefined ones.

1.2.1 delete vs private

在C++中,类的特殊函数,诸如拷贝构造函数,拷贝赋值函数等,默认如果有需要编译器会自动生成。如果某个函数明确不需要,那么可将该函数声明为private函数但不实现。只声明为private是不够的,因为成员函数或友元函数仍然可使用private函数。
在C++11中,更好的做法是将函数声明为public的delete函数,可通过=delete实现,delete函数被调用时将在编译时期报错,因此它的报错信息显得更好。

1.2.2 delete function

任何函数都可以声明为delete,利用这个特性,可以通过delete重载函数来避免不必要的类型转换,如下面的例子:

bool isLucky(int number); // original function
bool isLucky(char) = delete; // reject chars
bool isLucky(bool) = delete; // reject bools
bool isLucky(double) = delete; // reject doubles and floats

if (isLucky('a')) … // error! call to deleted function
if (isLucky(true)) … // error!
if (isLucky(3.5f)) … // error!

另一个应用在于删除模板函数中特定的类型,例如普通函数:

template
void processPointer(T* ptr);

template<>
void processPointer(void*) = delete;
template<>
void processPointer(char*) = delete;

或成员函数:

class Widget {
public:
    …
        template
    void processPointer(T* ptr)
    {
        …
    }
    …
};
template<> // still
void Widget::processPointer(void*) = delete; // public, but deleted

1.3 override

《Effective Modern C++》Item 12:
Declare overriding functions override.
C++的多态是通过virtual函数实现的,它要求派生类所override的函数要与基类的virtual函数具有

  • 相同名字(析构函数除外),
  • 相同的参数类型和数量,
  • 相同的constness
  • 返回类型和异常声明要和基类兼容

在C++11中,还要求有相同的引用标记,例如下列两个函数其实是不同的函数:

class Widget {
public:
    …
    void doWork() &; // this version of doWork applies
        // only when *this is an lvalue
    void doWork() && ; // this version of doWork applies
}; // only when *this is an rvalue
…
Widget makeWidget(); // factory function (returns rvalue)
Widget w; // normal object (an lvalue)
…
w.doWork(); // calls Widget::doWork for lvalues (i.e., Widget::doWork &)
makeWidget().doWork(); // calls Widget::doWork for rvalues (i.e., Widget::doWork &&)

再一个例子:

class Widget {
public:
    using DataType = std::vector;
    …
        DataType& data() & // for lvalue Widgets,
    {
        return values;
    } // return lvalue
    DataType data() && // for rvalue Widgets,
    {
        return std::move(values);
    } // return rvalue
    …
private:
    DataType values;
};

auto vals1 = w.data(); // calls lvalue overload for
// Widget::data, copy constructs vals1
auto vals2 = makeWidget().data(); // calls rvalue overload for
// Widget::data, move constructs vals2

如果派生类在定义virtual函数时不满足某个条件,编译不会报错,相当于定义了一个不同的函数。当在函数后面加上override时,表示告诉编译器该函数是在override基类的某个函数,如果编译器发现上述某个条件不满足则会报错。

class Base {
public:
    virtual void mf1() const;
    virtual void mf2(int x);
    virtual void mf3() &;
    virtual void mf4() const;
};
class Derived : public Base {
public:
    virtual void mf1() const override;
    virtual void mf2(int x) override;
    virtual void mf3() & override;
    void mf4() const override; // adding "virtual" is OK,
}; // but not necessary

1.4 final

final有两个作用

  • 用于成员函数时,表示该函数不能被override
  • 用于类时,表示该类不能被继承
struct Base
{
    virtual void foo();
};

struct A : Base
{
    void foo() final; // Base::foo is overridden and A::foo is the final override
    void bar() final; // Error: bar cannot be final as it is non-virtual
};

struct B final : A // struct B is final
{
    void foo() override; // Error: foo cannot be overridden as it is final in A
};

struct C : B // Error: B is final
{
};

1.5 using

《Effective Modern C++》Item 11:
Prefer alias declarations to typedefs.
C++11中的using能替代typedef的功能

typedef
std::unique_ptr>
UPtrMapSS;

using UPtrMapSS =
std::unique_ptr>;

// FP is a synonym for a pointer to a function taking an int and
// a const std::string& and returning nothing
typedef void(*FP)(int, const std::string&); // typedef same meaning as above
using FP = void(*)(int, const std::string&); // alias declaration

using的优势在于可直接支持模板,而typedef需要定义struct。

template // MyAllocList
using MyAllocList = std::list>; // is synonym for std::list>
MyAllocList lw; // client code

template // MyAllocList::type
struct MyAllocList { // is synonym for
    typedef std::list> type; // std::list>
MyAllocList::type lw; // client code
};

使用typedef的模板时

template
class Widget { // Widget contains
private: // a MyAllocList
    typename MyAllocList::type list; // as a data member
    …

这里,MyAllocList是一个依赖于模板参数T的类型,成为dependent type,C++要求使用dependent type时需要使用typename。用using时则显得更简单:

template
using MyAllocList = std::list>; // as before
template
class Widget {
private:
    MyAllocList list; // no "typename",
    … // no "::type"
};

1.6 noexcept

1.7 enum class(scoped enum)

1.8 mutable

在C++中,mutable修饰的变量可以被const函数读写。另外,在C++11中,对于用赋值capture的lambda函数,用mutable修饰后可以在函数体内修改所capture的变量值(当然,再函数外对应的变量是不会改变的),例如:

int x = 0;
auto f1 = [=]() mutable {x = 42;};  // OK
auto f2 = [=]()         {x = 42;};  // Error: a by-value capture cannot be modified in a non-mutable lambda

2 rvalue reference和move

2.1 什么是lvalue和rvalue?

lvalue是一种地址可知的表达式(如变量或者函数),通过该地址可以修改其数据,如下面的例子:

int a;
a = 1; // here, a is an lvalue

int x;
int& getRef () 
{
        return x;
}
 
getRef() = 4; // here, getRef() is an lvalue

rvalue则对应临时对象的表达式,其特点是

  • 在创建该临时对象的代码后面,临时对象会被销毁;
  • 在其生存的(即创建它的)范围内,无法得知对象的地址;
  • Rvalues不能绑定到lvalue references,但能绑定到const的lvalue reference。
int x;
int getVal ()
{
    return x;
}
getVal();//getVal() is an rvalue

string getName ()
{
    return "Alex";
}
getName(); //getName returns a string that is constructed inside the function, rvalue

template
nvoid f(T&& param);
f(27); //27 is rvalue

2.2 rvalue reference

在C++11里,rvalue reference是为了提高用临时对象构建新对象的效率而引入的feature。我们以string为例,了解下这种低效的对象构建:

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = std::strlen(p) + 1;
        data = new char[size];
        std::memcpy(data, p, size);
    }
    ~string()
    {
        delete[] data;
    }

    //copy constructor
    string(const string& that)
    {
        size_t size = std::strlen(that.data) + 1;
        data = new char[size];
        std::memcpy(data, that.data, size);
    }
 }

当遇到下面的使用时,Line 2和Line 3首先创建一个临时对象,然后调用string的拷贝构造函数,最后临时对象被销毁。

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

实际上,这种方式明显效率较低,如果给定的参数是临时对象,那么构造函数完全不需要经历分配-拷贝-释放参数内存这样的过程,完全可以把它对应的内存数据“偷”过来,这就是move。注意move的目标是含有指向内存的指针,如果对象所有成员都是内置类型(如int,float),那么move和copy其实是一回事。问题的关键在于如何能识别出临时对象?

  • C++11中引入的rvalue reference,是一种专门绑定临时对象的reference,其语法是T&&。
  • 在C++11之前的普通引用T&(lvalue reference)无法区分地去绑定对象。

所以,如果同时定义两个overload函数,参数类型分别为lvalue reference和rvalue reference,那么就可以区分出临时对象和非临时对象,从而可实现期望的行为。rvalue reference可以是const的,但const T&&很少用,可以忽略。

2.3 Move constructor和move assignment

rvalue reference最常见的应用是move constructor和move assigment。当一个对象中有指针成员,且指向内存中某个地址时,若用一个临时对象作为参数来构造一个新的对象,那么最有效的方式是把临时对象的指针获取给新的对象,然后将临时对象的指针设为null,这样就无须allocate新的内存并复制。接着之前string的例子,对应的move constructor和move assignment为

string(string&& that):data(that.data)   // string&& is an rvalue reference to a string
{        
    that.data = nullptr;
}
string& operator=(string that)
{
    std::swap(data, that.data);
    return *this;
}

注意,move constructor的参数是rvalue reference,但rvalue reference本身是一个lvalue,因为它具有名字,地址。如果用rvalue reference的数据去初始化另一个对象,那么调用的将是copy constructor,如果没有copy constructor,则编译器会报错。例如,

class Foo
{
    unique_ptr member;

public:

    Foo(unique_ptr&& parameter)
    : member(parameter)   // error
    {}
};

如果编程者很清楚需要用的是move constructor,可以用std::move显式地将lvalue转化为rvalue,同时注意到之后就被release了,使用std::move的编程者要清楚这样的结果。

class Foo
{
    unique_ptr member;

public:

    Foo(unique_ptr&& parameter)
    : member(std::move(parameter))   // note the std::move
    {}
};

2.4 universal reference

T&&并不一定是绑定到rvalue的。《Effectuve Modern C++》的Item24中提到,当出现type deduction时,T&&既可以绑定lvalue,又可以绑定rvalue。此时,它被称为universal reference(或forward refernce)。universal reference包括两种形式——template和auto:

template
void f(T&& param); // param is a universal reference

Widget&& var1 = Widget(); // rvalue reference
auto&& var2 = var1; // var2 is universal reference

需要注意的是,并非带有template的T&&就一定是universal reference,必须是很严格的T&&形式并且有type deduction才行,以下几种情况都是rvalue reference,而非univeral reference:

template
void f(std::vector&& param); //universal reference should be T&& rather than vector&&

template
void f(const T&& param); //universal reference should be T&& rather than const T&&

template> // from C++~~~~
class vector { // Standards
public:
void push_back(T&& x); //no type deduction
…
};

2.5 std::move

std::move用于把lvalue转化为rvalue,转化后的结果通常用于move(这里的move是指把memory中的resource移交给另一个对象),这是一种显示的转换,使用std::move编程者在告诉编译器,自己很清楚原来的rvalue会被move,清楚去避免对lvalue再执行危险操作。std::move的实现代码如下:

template //C++11
typename std::remove_reference::type&&
move(T&& t)
{
    return static_cast::type&&>(t);
}

template // C++14; 
decltype(auto) move(T&& param) 
{
return static_cast&&>(param);
}

可以看到,std::move接收的参数类型是universal reference,即绑定lvalue或rvalue,返回类型是rvalue reference。但乍看难以明白remove_reference_t做了什么。事实上,这里涉及两个Modern C++的要点:type deduction和reference

  • 对于universal reference,如果传进来的是lvalue,那么编译器推导出的类型T为对应类型的引用;如果传进来的是rvalue,那么T为对应的类型,例如《Effective Modern C++》Item28中的例子
template
void func(T&& param);

Widget widgetFactory(); // function returning rvalue
Widget w; // a variable (an lvalue)
func(w); // call func with lvalue; T deduced
// to be Widget&
func(widgetFactory()); // call func with rvalue; T deduced
// to be Widget
  • 接着上面的例子,当传进lvalue时,最终编译器推导出的函数void func(Widget& && param),但是C++是不支持双重引用类型的,这时编译器会通过reference collapse将之转换为单引用,其规则是
如果两个引用中至少有一个是lvalue reference,那么结果是lvalue reference。否则 (即两个引用都是rvalue references)结果为rvalue reference.

注意,调用一个返回rvalue reference的函数,得到的是rvalue,更准确地说是得到xvalue,不同于普通的rvalue(prvalue, pure rvalue),返回rvalue的函数并没有创建一个临时对象,这迫使C++引入xvalue这样一种特殊分类——它可以绑定到rvalue reference,但是又有别于传统的rvalue。
回到之前的问题,对于传进std::move的lvalue,remove_reference_t去除了其引用,保证了范围结果是rvalue,否则根据reference collapse,最后结果将是lvalue reference。

2.6 std::forward

std::forward是为了解决参数等效传送的需要,即forwarding problem。假设函数(或类)f(a, b, ... , c)中调用E(a, b, ... , c),并且希望函数f和E接收的参数类型是完全等效的,这在C++03是无法用一个函数实现的,我们看下std::forward最常见的应用形式:

template
void f(T&& fParam)
{
… // do some work
someFunc(std::forward(fParam)); // forward fParam to
} // someFunc

std::forward利用type deduction和reference collapse,推导出fParam是lvalue还是rvalue,如果是rvalue(?reference),则将fParam转化为rvalue(?reference),std::forward的如下:

//C++11
template 
T&& forward(typename 
remove_reference::type& param) 
{
return static_cast(param);
}

//C++14
template // C++14; still in
T&& forward(remove_reference_t& param) // namespace std
{
return static_cast(param);
}

明显,当一个参数传进一个接收参数类型为universal reference的函数后,在函数内该参数其实已经是个lvalue,但利用

3 Smart Pointer

Smart Pointer是包装了C++指针的类,用于管理指针所指资源的生命周期。编程者必须把smart pointer分配在在栈上(即局部变量),这样当程序离开smart pointer所在的scope时,随着smart pointer被销毁,其管理的资源也会被释放。相比之下,C++指针可能因为提前退出或抛出异常等原因导致delete没执行而造成内存泄漏,例如下面的例子

#include 

void someFunction()
{
    Resource *ptr{ new Resource() };

    int x{};
    std::cout << "Enter an integer: ";
    std::cin >> x;

    if (x == 0)
        throw 0; // the function returns early, and ptr won’t be deleted!

    // do stuff with ptr here
    delete ptr;
}

C++11有三个smart pointer:unique_ptr,shared_ptr和weak_ptr,其中,unique_ptr最常见。

3.1 unique_ptr

3.1.1 std::unique_ptr

正如名字,unique_ptr所管理的动态分配资源不能被多个对象共用,它通过禁用copy constructor和copy assignment,保证动态资源只能从一个unique_ptr move到另一个unique_ptr。注意,move操作接收参数是ravlue,因此将unique_ptr的动态资源移交给其他对象时,需要用std::move把lvalue转化为rvalue。

class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
};

int main()
{
    std::unique_ptr res1{ new Resource{} }; // Resource created here
    std::unique_ptr res2{}; // Start as nullptr

    // res2 = res1; // Won't compile: copy assignment is disabled
    res2 = std::move(res1); // res2 assumes ownership, res1 is set to null

    return 0;
} // Resource destroyed here when res2 goes out of scope
3.1.2 std::unique_ptr和数组

std::unique的形式来管理动态的数组资源,但这种方式是不建议使用的,因为std:array,std::vecor和std::string是个更好的选择。

3.1.3 std::make_unique

std::make_unique是C++14的一个函数,用于根据模板类型和参数创建并返回一个unique_ptr:

template
std::unique_ptr make_unique(Ts&&... params)
{
    return std::unique_ptr(new T(std::forward(params)...));
}

std::make_unique是modern C++建议的创建unique_ptr的方式,因为

  • 书写上更加简洁
std::unique_ptrobj{new T};
auto obj{std::make_unique()};
  • 应对异常时更加安全

在下面的例子中,对于Line1,编译器可能会先创建new T,然后执行function_that_can_throw_exception(),最后创建unique_ptr,这样如果函数跑出异常,那么new T的资源将无法释放,而Line2使用make_unique则没有这样的问题。

some_function(std::unique_ptr(new T),  function_that_can_throw_exception()); //Line1
some_function(std::make_unique(),  function_that_can_throw_exception()); //Line2
3.1.4 从函数返回unique_ptr

当需要从函数返回unique_ptr时,按值返回是安全的。如果返回的unique_ptr没赋给其他unique_ptr,那么该临时unique_ptr会自然地随着退出scope而释放掉。如果被拿去赋值,如下面的例子,则其资源会被move到新的unique_ptr上。

std::unique_ptr  createResource()
{
 return  std::make_unique();
}

int  main()
{
    std::unique_ptr  ptr{  createResource()  };

    //do whatever
    return  0;
}

通常,不应该返回unique_ptr的指针(决不能)或者引用(除非有足够的理由)

3.1.5 将unique_ptr传进函数中

由于unique_ptr没有copy assigment,所以需要通过std::move转化为rvalue然后传值给函数,这是资源的所有权发生了move。如果不想转交所有权,那么可通过unique_ptr的get()函数直接把资源直接传进函数中。

3.1.6 unique_ptr的错误用法

因为unique_ptr在离开其scope时会释放其管理的资源,所以不能用同一个C++指针去初始化两个不同的unique_ptr,这样会导致无定义行为,下面的例子即错误的用法:

Resource *res{ new Resource() };
std::unique_ptr res1{ res };
std::unique_ptr res2{ res };//error

另外,对于用于初始化unique_ptr的 C++指针,不能手动去delete它指向的资源。

Resource *res{new Resource()};
std::unique_ptr res1{res};
delete res; //error

3.2 shared_ptr

与unique_ptr不同,shared_ptr用于多个指针管理一个资源的应用场景,并且只有当所有指向该资源的share_ptr都离开scope后,资源才会被释放。

3.2.1 std::shared_ptr

注意,不能用指向资源的C++指针去初始化多个shared_ptr,否则会发生重复释放的undefined行为,正确用法是用shared_ptr通过copy constructor创建新的shared_ptr,这样资源被引用的次数会被正确记录,正确用法:

int main()
{
    // allocate a Resource object and have it owned by std::shared_ptr
    Resource *res = new Resource;
    std::shared_ptr ptr1(res);
    {
        std::shared_ptr ptr2(ptr1); // use copy initialization to make another std::shared_ptr pointing to the same thing
    } // ptr2 goes out of scope here, but nothing happens

    return 0;
} // ptr1 goes out of scope here, and the allocated Resource is destroyed

错误用法:

int main()
{
    Resource *res = new Resource;
    std::shared_ptr ptr1(res);
    {
        std::shared_ptr ptr2(res); // create ptr2 directly from res (instead of ptr1)
    } // ptr2 goes out of scope here, and the allocated Resource is destroyed

    return 0;
} // ptr1 goes out of scope here, and the allocated Resource is destroyed again
3.2.2 std::make_shared

与用std::make_unique创建unique_ptr类似,std::make_shared是C++11建议的用来创建shared_ptr的函数,它可以避免上一小节中的错误用法,更加安全:

int main()
{
    // allocate a Resource object and have it owned by std::shared_ptr
    auto ptr1 = std::make_shared();
    {
        auto ptr2 = ptr1; // create ptr2 using copy initialization of ptr1
    } // ptr2 goes out of scope here, but nothing happens

    return 0;
} // ptr1 goes out of scope here, and the allocated Resource is destroyed
3.2.3 shared_ptr的底层原理

std::shared_ptr内部有两个指针,一个指向所管理的资源,另一个指向“control block”,control block用于追踪一系列状态,其中包括指向资源指针的数量。当通过构造函数创建shared_ptr时,资源和control block是单独分配的。而如果用std::make_shared(),则只需要一次内存分配,效率更高。在前面提到的错误用法中,当用同一个资源指针去初始化两个shared_ptr时,实际上创建了两个control block,追踪各自的计数器,因此最终在各自计数器为0时会分别释放资源,造成资源泄露。

3.2.4 用unique_ptr创建shared_ptr

shared_ptr有一个特殊的构造函数,接收一个std::unique_ptr的rvalue作为参数,将unique_ptr的资源move到shared_ptr中。注意,反过来,shared_ptr是不能转化为unique_ptr的。

3.2.5 shared_ptr的错误用法

std::shared_ptr和std::unique_ptr在被使用时,有一些相同的危险性:当std::shared_ptr没有被正确析构时,其管理的资源将得不到释放。可能的原因是std::shared_ptr是动态分配的,并且没有被delete;或者它是一个对象的成员,但是该对象是动态分配且没被delete。

3.3 weak_ptr

在使用std::shared_ptr时,可能会出现circular reference:对象之间的引用构成环状。这里的引用是一个广义的概念,对于std::shared_ptr而言是pointer。
image.png
下面是一个例子:

class Person
{
    std::string m_name;
    std::shared_ptr m_partner; // initially created empty

public:

    Person(const std::string &name) : m_name(name){}
    ~Person(){}

    friend bool partnerUp(std::shared_ptr &p1, std::shared_ptr &p2)
    {
        if (!p1 || !p2)
            return false;

        p1->m_partner = p2;
        p2->m_partner = p1;

        return true;
    }
};

int main()
{
    auto lucy = std::make_shared("Lucy"); // create a Person named "Lucy"
    auto ricky = std::make_shared("Ricky"); // create a Person named "Ricky"

    partnerUp(lucy, ricky); // Make "Lucy" point to "Ricky" and vice-versa

    return 0;
}

在该例子中,lucy和ricky.m_partner都指向了资源lucy,ricky和lucy.m_partner都指向了资源ricky,但因为circular reference的存在,析构lucy时只将资源lucy的计数器减1,析构ricky时直降资源ricky的计数器减1,最终两个资源都得不到释放。
std::weak_ptr通过copy一个std::shared_ptr来创建,它能访问一个或多个std::shared_ptr管理的资源,但是它不会使计数器增加。其创建和析构对其他std::shared_ptr没有影响,因此它可以把circular reference避免。

4 functional

functional的官方教程有cppreferencecplusplus

4.1 Function Object

函数对象常用于实现Callback,Callback是一个作为参数传递给另一个API的函数,让被调用的API在某个点执行该Callback函数,C++实现Callback有三种方式:

  • 函数指针
  • 函数对象
  • Lambda函数

其中,函数对象于函数指针相比,其优势在于它能封装状态。函数对象是指一个重载了opertor()的类对象:

class MyFunctor
{
public:
    int operator()(int a, int b)
    {
        return a + b;
    }
};

MyFunctor funObj;
std::cout << funObj(2, 3) << std::endl;

4.2 std::bind

std::bind是一个函数adaptor,它接收一个函数作为输入,经过将参数绑定或调整后,返回一个新的函数对象。

int add(int first, int second)
{
    return first + second;
}

auto add_func = std::bind(&add, _1, _2);
auto new_add_func = std::bind(&add, 12, _1);
auto mod_add_func = std::bind(&add, _2, _1);
std::function mod_add_funcObj = std::bind(&add, 20, _1);

这里,

  • add_func依次绑定第一个和第二个参数,因此,它和add()是等效的。
  • new_add_func是一个接收一个参数的函数对象,它调用add(),并将12和它的参数依次作为add()的两个参数。
  • auto mod_add_func两个参数顺序调换,mod_add_func(12, 15)等效于add(15,12)
  • std::bind返回的是函数对象,因此它可用std::function保存

4.3 std::function

在C++11里,std::function是一种函数wrapper,它能存储,赋值以及触发任何Callable。Callable包括函数,lambda表达式,std::bind表达式,函数对象以及指向成员函数的指针。

4.3.1 调用callback

包装在std::function里的callable通过operator()调用,下面的例子

using cb1_t = std::function;
void foo1() { ... }
void foo2(int i) { ... }

cb1_t f1 = std::bind(&foo1);
cb1_t f2 = std::bind(&foo2, 5);

f1(); // Invoke foo1()
f2(); // Invoke foo2(5)

4.4 std::reference_wrapper

std::reference_wrapper是一个包装了引用的类模板,且可复制,可赋值。std::reference_wrapper能够隐式地转化为T&,因此它可作为接收T&函数的参数。std::reference_wrapper常用于:

  • 在标准的容器中(如std:vector,std::pair)中存储引用
  • 按引用的方式传递对象给std::bind或者std::thread的构造函数

一个例子:

#include 
#include 
#include 
#include 
#include 

int main()
{
    std::list l = { -4, -3, -2, -1, 0, 1, 2, 3, 4 };

    std::vector> v(l.begin(), l.end());
    std::random_shuffle(v.begin(), v.end());

    std::vector> v2(v.begin(), v.end());
    std::partition(v2.begin(), v2.end(), [](int n) {return n < 0; });

    std::cout << "Contents of the list: ";
    for (int n : l) {
        std::cout << n << ' ';
    }
    std::cout << '\n';

    std::cout << "Contents of the list, shuffled: ";
    for (int i : v) {
        std::cout << i << ' ';
    }
    std::cout << '\n';

    std::cout << "Shuffled elements, partitioned: ";
    for (int i : v2) {
        std::cout << i << ' ';
    }
    std::cout << '\n';
}

4.5 std::ref和std::cref

std::ref和std::cref是函数模板,用于生成类型为std::reference_wrapper的对象,它利用模板参数推导出参数的类型。

例子:

#include 
#include 

void f(int& n1, int& n2, const int& n3)
{
    std::cout << "In function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
    ++n1; // increments the copy of n1 stored in the function object
    ++n2; // increments the main()'s n2
    // ++n3; // compile error
}

int main()
{
    int n1 = 1, n2 = 2, n3 = 3;
    std::function bound_f = std::bind(f, n1, std::ref(n2), std::cref(n3));
    n1 = 10;
    n2 = 11;
    n3 = 12;
    std::cout << "Before function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
    bound_f();
    std::cout << "After function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
}

其结果为:

Before function: 10 11 12
In function: 1 11 12
After function: 10 12 12

5 type deduction

这一章节总结下《Effective Modern C++》中type deduction的内容。

5.1 template type deduction

template type deduction有三种情况:

  • 参数类型是指针或非universal的引用
  • 参数类型是universal引用
  • 参数类型既非指针也非引用

5.1.1 参数类型是非universal的引用或指针

这种情况如下

template
void f(T& param); // param is a reference

在type deduction过程中,传入参数如果是引用,则引用属性会被忽略,然后进行类型匹配,如下面例子:

int x = 27; // x is an int
const int cx = x; // cx is a const int
const int& rx = x; // rx is a reference to x as a const int

f(x); // T is int, param's type is int&
f(cx); // T is const int, param's type is const int&
f(rx); // T is const int, param's type is const int&

在上面例子中,把cx和rx的T推导成const int是合理的,否则函数内部能修改cx和rx,与其原来的const属性矛盾。
如果函数模板的参数类型已有const,则推导出的T不带有const:

template
void f(const T& param); // param is now a ref-to-const
int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // T is int, param's type is const int&
f(cx); // T is int, param's type is const int&
f(rx); // T is int, param's type is const int&

5.1.2 universal引用

对于形如T&&的universal引用,如果传入参数是rvalue,那么可归类为5.1的情况,如果传入参数是lvalue,那么T会被推导为lvalue reference。如下面例子:

template
void f(T&& param); // param is now a universal reference
int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // x is lvalue, so T is int&, param's type is also int&
f(cx); // cx is lvalue, so T is const int&, param's type is also const int&
f(rx); // rx is lvalue, so T is const int&, param's type is also const int&
f(27); // 27 is rvalue, so T is int, param's type is therefore int&&

可以这么理解,

5.1.3 既非指针也非引用

这种情况传入参数的引用、const和volatile属性在推导时都会被忽略,这个很好理解,因为这种情况是传值,传值后的参数自然是独立的(没有引用性质),可修改的:

template
void f(T param); // param is now passed by value

int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // T's and param's types are both int
f(cx); // T's and param's types are again both int
f(rx); // T's and param's types are still both int

5.1.4 数组

数组作为传值模板的函数参数时会退化成指针,作为传引用模板的函数参数时则推导成数组引用:

const char name[] = "J. P. Briggs"; // name's type is const char[13]
const char * ptrToName = name; // array decays to pointer

template
void f(T param); // template with by-value parameter
f(name); //const char*

template
void f(T& param); // template with by-reference parameter
f(name); //const char(&)[13]

5.1.5 函数

函数作为传值模板的参数时推导为函数指针,作为传引用模板的参数时推导为函数的引用:

void someFunc(int, double); // someFunc is a function; type is void(int, double)
template
void f1(T param); // in f1, param passed by value
template
void f2(T& param); // in f2, param passed by ref
f1(someFunc); // param deduced as ptr-to-func; type is void (*)(int, double)
f2(someFunc); // param deduced as ref-to-func; type is void (&)(int, double)

5.2 auto type deduction

auto相当于template type deduction中的T,唯一的区别是当用{}初始化变量时,auto会推导成std::initializer_list,而template则无法通过编译。

auto x1 = 27; // type is int, value is 27
auto x2(27); // ditto
auto x3 = { 27 }; // type is std::initializer_list, value is { 27 }
auto x4{ 27 }; // ditto

5.3 decltype

decltype几乎是无修改地推导出变量或表达式的类型,对于类型为T的非名字的lvalue表达式,decltype推导为T&.
C++14支持decltype(auto), auto表示类型要被推导,而decltype表示推导时使用decltyp的规则,即原始类型。

template // C++14; works,
decltype(auto) // but still
authAndAccess(Container& c, Index i) // requires
{ // refinement
    authenticateUser();
    return c[i];
}

Widget w;
const Widget& cw = w;
auto myWidget1 = cw; // auto type deduction:
decltype(auto) myWidget2 = cw; // decltype type deduction:myWidget2's type is const Widget&

6 auto

6.1 auto的优点和适用场景

6.1.1 必须初始化

用auto修饰的变量必须初始化,否则编译阶段会报错

int x1; // potentially uninitialized
auto x2; // error! initializer required
auto x3 = 0; // fine, x's value is well-defined

6.1.2 简洁

对比下面两个例子:

template // algorithm to dwim ("do what I mean")
void dwim(It b, It e) // for all elements in range from
{ // b to e
    while (b != e) {
        typename std::iterator_traits::value_type
            currValue = *b;
        …
    }
}

template // as before
void dwim(It b, It e)
{
    while (b != e) {
        auto currValue = *b;
        …
    }
}

6.1.3 比std::function更好

同样可以用来保存Lambda函数,但是auto比std::function使用更少的空间,而且执行起来更快:

auto derefLess = // C++14 comparison
[](const auto& p1, // function for
    const auto& p2) // values pointed
{ return *p1 < *p2; }; // to by anything pointer-like

6.1.4 类型的兼容性

用显式地类型接收函数返回值时,如果类型不匹配则可能导致类型转换,存在潜在的错误,用auto变量接收函数返回值则避免该问题,而且如果函数修改了返回类型,那么使用该函数的auto代码无须修改。

auto sz = v.size(); // sz's type is std::vector::size_type
for (const auto& p : m)
{
    … // as before
}

7 Lambda

7.1 Lambda的原理

关于lambda的基本语法这里就不接介绍了,这里先记下其原理。定义lambda时实际上定义了一个重载了operator()的类,并创建该类的一个对象。在该类被创建时,其所在的环境中的变量被传进类的构造函数并保存为成员变量,这种实现和functor类似。lambada最大的优点在于代码简单
Lambda capture的方式如下:

[]    Capture nothing(or , a scorched earth strategy ? )
[&]    Capture any referenced variable by reference
[=]    Capture any referenced variable by making a copy
[=, &foo]   Capture any referenced variable by making a copy, 
            but capture variable foo by reference
[bar]      Capture bar by making a copy; don't copy anything else
[this]     Capture the this pointer of the enclosing class

lambda只能capture局部变量,不能capture类的成员变量。不过通过[this]先capture到this指针,那么可以让lambda的实现代码看上去使用成员变量和局部变量没有区别。如下面的例子:

class Widget {
public:
    … // ctors, etc.
        void addFilter() const; // add an entry to filters
private:
    int divisor; // used in Widget's filter
};

void Widget::addFilter() const
{
    filters.emplace_back( // error!
        [](int value) { return value % divisor == 0; } // divisor not available
    );  
} 

void Widget::addFilter() const
{
    filters.emplace_back(
        [=](int value) { return value % divisor == 0; }
    );
}

7.2 Lambda的应用场景

7.2.1 STL算法

vector v;
v.push_back(1);
v.push_back(2);
//...
for_each(v.begin(), v.end(), [](int val)
{
    cout << val;
});

7.3 Lambda的使用建议

这一节主要罗列下《Effective Modern C++》里对lambda使用习惯的建议。

7.3.1 避免默认的capture方式

C++11有两种capture方式:传值和传引用。

7.3.1.1 容易造成dangle reference/pointer

使用默认的传引用方式所引用的局部变量在退出函数被释放,当从一个函数中返回lamabda函数时,导致所capture的变量无效,引发无定义行为。

void addDivisorFilter()
{
    auto calc1 = computeSomeValue1();
    auto calc2 = computeSomeValue2();
    auto divisor = computeDivisor(calc1, calc2);
    filters.emplace_back( // danger!
        [&](int value) { return value % divisor == 0; } // ref to
    ); // divisor will dangle!
} 

使用默认的传值方式capture一个局部指针也有类似的问题。当然,即使不是默认方式,而是capture某个特定的变量也有相同的dangle问题,但是这样能从字面上提醒编程者所capture变量的有效范围,更容易察觉到问题。

7.3.1.2 容易造成对成员变量capture的误解

lambda只能capture临时变量和类实例的this指针,无法capture类的数据成员。有意思的是,当使用默认的capture方式并在lambda的函数体内使用成员变量,编译器并不报错,如下面的例子:

class Widget {
public:
    … // ctors, etc.
        void addFilter() const; // add an entry to filters
private:
    int divisor; // used in Widget's filter
};

void Widget::addFilter() const
{
    filters.emplace_back(
        [=](int value) { return value % divisor == 0; }
    );
}

事实上,默认的capture方式连同this指针capture进去,然后再通过this指针访问成员变量divisor。这种方式容易误导,建议使用更明确的capture,如

void Widget::addFilter() const
{
    filters.emplace_back( // C++14:
        [divisor = divisor](int value) // copy divisor to closure
    { return value % divisor == 0; } // use the copy
    );
}

void Widget::addFilter() const
{
    filters.emplace_back( // C++14:
        [&divisor = divisor](int value) // copy divisor to closure
    { return value % divisor == 0; } // use the copy
    );
}
7.3.1.3 容易造成对static变量的误解

全局变量,namespace范围以及用类或函数中用static修饰的变量,都具有static存储周期,这种变量能直接在lambda中使用,但无法被capture,因此使用默认的capture容易造成误解,如下面例子:

void addDivisorFilter()
{
    static auto calc1 = computeSomeValue1(); // now static
    static auto calc2 = computeSomeValue2(); // now static
    static auto divisor = // now static
        computeDivisor(calc1, calc2);
    filters.emplace_back(
        [=](int value) // captures nothing!
    { return value % divisor == 0; } // refers to above static
    );
    ++divisor; // modify divisor
}

7.3.2 使用init capture来move实例

对于move-only的实例(例如std::unique_ptr)或者copy代价高但move很简便的实例,在C++14中,可以通过init capture将之move到lambda中:

class Widget { // some useful type
public:
    …
    bool isValidated() const;
    bool isProcessed() const;
    bool isArchived() const;
private:
    …
};

auto pw = std::make_unique(); // create Widget; 
… // configure *pw
auto func = [pw = std::move(pw)] // init data mbr
{ return pw->isValidated() && pw->isArchived(); };

7.3.3 对auto&&类型的参数使用decltype来将之forward

C++14有一个很实用的feature:generic lambda,即lambda的参数可以是auto类型。其实现很直接:lambda的类中,operator()是一个template,例如对于

auto f = [](auto x) { return func(normalize(x)); };

其operator()的实现为:

class SomeCompilerGeneratedClassName {
public:
    template // see Item 3 for
    auto operator()(T x) const // auto return type
    {
        return func(normalize(x));
    }
    … // other closure class
}; // functionality

如果想forward参数,那么应该这么定义lambda函数:

auto f =
[](auto&& param)
{
    return
        func(normalize(std::forward(param)));
};

其原理是什么呢?首先回顾下std::forward的机制:

//C++14
template // C++14; still in
T&& forward(remove_reference_t& param) // namespace std
{
return static_cast(param);
}

正常情况下,T&& param表示的universal reference的参数param,

  • 如果传进的类型为type的lvalue,那么编译器推断T=type&,
  • 如果传进的是类型为type的rvalue

,那么编译器推断T=type
因此,根据reference collapse,std::forward(param)对于前者返回type&类型,对于后者返回type&&类型。但是如果对于rvalue,让T=tpye&&,那么返回类型依然是type&&,因此,用std::forward(param)是正确的。

7.3.4 lambda胜过于std::bind

lambda胜过于std::bind:

  • lambda可读性强于std::bind
  • lambda在一些情况下性能优于std::bind

你可能感兴趣的:(c++11)