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
- 用于成员函数时,表示该函数不能被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
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
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
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。
下面是一个例子:
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的官方教程有cppreference和cplusplus。
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
7.3.4 lambda胜过于std::bind
lambda胜过于std::bind:
- lambda可读性强于std::bind
- lambda在一些情况下性能优于std::bind