相比传统的C++98与C++03, C++11中提出了很多新的概念,本文根据Scott Meyers 在Youtube上的培训视频展开,介绍C++11中的一些典型概念。
1. 左值(lvalue)与右值(rvalue)
C++11之前已经有左值与右值的概念,但由于其只是简单的概念,并无太多推广应用,关注的不多;C++11中则贯穿了左值和右值的相关应用,比如类型推断,完美转发(Perfect Forwarding),移动语义(Move Semantic)等。判断左右值的依据非常简单:
如果一个对象可以取地址,就是左值,反之就是右值。
基于左值和右值,出现了左值引用和右值引用,它们广泛的被应用到类型推断中。
左值引用就是对左值的引用,右值引用就是对右值的引用。
唯一的例外是常量左值引用可以绑定到右值。
int a = 3;
int& b = a; //左值引用
int&& c = 3; //右值引用
int&& d = int(3); //右值引用
2. 类型推断(type deduction)
类型推断在C++98中就有,C++11中将其扩大化,它们的关系如下图所示:
C++11在C++98的基础上引入了广义引用T&&(Universal Reference),decltype,并引入了auto object; 另外,扩展C++98中T&/T*等到lamada按值或按引用捕获,扩展T到lamada隐式返回等。
2.1 模板类型推断(template type deduction)
模板类型推断的一般形式如下:
template <typename T>
void f(ParamType param)
模板类型推断就是根据函数f(expr)调用中expr的类型来推断ParamType以及T的类型;通常有以下几种情形:
2.1.1 ParamType类型是广义引用(Universal Reference)的情形
ParamType类型是广义引用的具体的表现形式为:
template<typename T>
void f(T&& param); // param is a universal reference
广义引用被绑定到左值或右值时,进行类型推导的规则不同,例如,调用f(expr)时:
- 如果expr是左值,那么T和ParamType将会被推导为左值引用
- 如果expr是右值,那么ParamType将会被推导为右值引用, T根据实际类型决定
具体的:
int x = 27;
const int cx = x;
const int& rx = x;
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&&
虽然广义引用和右值引用均有相同的&&符号,但它们是有区别的:右值引用只能绑定到右值,广义引用可以绑定到左值或者右值;广义引用只在类型推导中产生并且要求T的所有类型均是推导出来的,否则其是右值引用,例如:
void f(Widget&& param); // rvalue reference,param's type is fixed and deduction is not happened
Widget&& var1 = Widget(); // rvalue reference
auto&& var2 = var1; // lvalue reference(var1's type is rvalue ref,
// but var1 itself is a lvalue since it has name and can take address
template<typename T>
void f(std::vector<T>&& param); // rvalue reference
template<typename T>
void f(T&& param); // not rvalue reference,it's universal reference
区别广义引用和右值引用,只需记住如下几点:
- 对于待推导的类型,如果一个函数模板的模板参数类型是T&&或者一个对象声明用的是auto &&, 那么模板参数或者对象就是广义引用;
- 如果没有类型推导或者类型推导的参数类型不是T&&或者auto &&(例如参数类型是const T&&或者声明对象用的是const auto&&),那么模板参数或对象就是右值引用;
- 如果参数类型是右值,广义引用等价于右值引用;如果参数是左值,等价于左值引用
2.1.2 ParamType类型是引用或指针, 但不是广义引用的情形
ParamType类型是引用或指针的具体表现形式为:
template<typename T>
void f(T& param); // param is a reference
调用f(expr)时进行类型推导的规则如下:
- 如果expr的类型是引用,那么忽略引用部分进行类型推导
- 然后用模式匹配的方式分析expr的类型和ParamType的类型修饰符来决定T的类型
例如:
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&;rx’s type is a reference, T is deduced to
be a non-reference, because rx’s reference-ness is ignored during type deduction
如果将模板参数中的T&变为const T&, 情况也类似,对于:
template<typename T>
void f(const T& param); // param is now a ref-to-const
调用f(expr)时进行类型推导时,规则和上面相同,只是在进行模式匹配推导T时模式稍有不同(多了const), 例如:
int x = 27;
const int cx = x;
const int& rx = x;
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&
对于参数类型为指针T*的情形,类型推导规则类似:
template<typename T>
void f(T* param); // param is now a pointer
int x = 27;
const int *px = &x; // px is a ptr to x as a const int
f(&x); // T is int, param's type is int*
f(px); // T is const int, param's type is const int*
2.1.3 ParamType不是引用或者指针的情形
函数模板如果按照下面pass-by-value的方式进行:
template<typename T>
void f(T param); // param is now passed by value
那么则意味着参数param将会获得实参的一个副本,也就是说不管f(expr)调用时其类型是什么,const,volatile,引用,param的变化将不会对实参有任何影响,那么按照这种pass-by-value的方式进行的类型推导规则也就非常清晰了:
- 如果expr的类型含有引用,忽略引用部分进行推导;
- 如果忽略引用部分后还有const, volatile 等,忽略这些部分进行类型推导
例如:
int x = 27;
const int cx = x;
const int& rx = x;
const int* px = &x;
const int* pcx const= &x;
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
f(px); // T's and param's types are both const int *
f(pcx); // T's and param's types are both const int *
这里要注意,对于指针的情况,如果是指针常量,因为是按值传递,指针的常量属性将会被忽略;如果是常量指针,则这个属性会保留;例如上面的px, 不能通过它改变它指向的对象;pcx的类型是const int * const, 指针的常量属性在按值传递时会被忽略,但是常量指针的性质会被保留(param的类型为const int*),不能通过它改变它指向的对象。
对于数组名,数组类型在按值传递时会退化为指针:
const char name[] = "J. P. Briggs"; // name's type is const char[13]
const char* ptrToName = name; // array decays to pointer const char*
f(name); // T's and param's types are both const char*
但是,数组类型在按引用传递时会保持数组类型:
template<typename T>
void f(T& param); // template with by-reference parameter
对于上面的按引用传递:
f(name); // T's type is const char[13], param's type is const char(&)[13]
按引用传递不会丢失数组容量的这个特征可以应用到模板中,使得可以在编译期获得数组容量,:
// 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<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
return N;
}
2.2 auto type deduction
auto类型推断和template类型推断虽然形式不同,但是原理相同。auto类型推断在声明一个对象时,auto等同与template类型推断中的T, 对象的类型描述符等同于模板类型推断中的ParamType。同样,template类型推断的三种情形也可以应用到auto类型推断。
例如,对于2.1.2和2.1.3的情形:
template<typename T>
void f(ParamType param);
auto x = 27; // auto == T and auto == ParamType;
const auto cx = x; // auto == T and const auto == ParamType
const auto& rx = x; // auto == T and const auto& == ParamType
template<typename T> // conceptual template for deducing x's type
void func_for_x(T param);
template<typename T> // conceptual template for deducing cx's type
void func_for_cx(const T param);
template<typename T> // conceptual template for deducing rx's type
void func_for_rx(const T& param);
对于2.1.1对应的广义引用规则,auto类型推断的情形如下:
auto x = 27;
const auto cx = x;
const auto& rx = x;
auto&& uref1 = x; // x is int and lvalue, so uref1's type is int&
auto&& uref2 = cx; // cx is const int and lvalue, so uref2's type is const int&
auto&& uref3 = 27; // 27 is int and rvalue, so uref3's type is int&&
对于数组退化为指针的情形,auto类型推断同样适用:
const char name[] = "R. N. Briggs"; // name's type is const char[13]
auto arr1 = name; // arr1's type is const char*
auto& arr2 = name; // arr2's type is const char (&)[13]
值得注意的是,auto类型推断在初始化列表类型推断和template类型推断时有不能完全等同:
auto x = { 11, 23, 9 }; // x's type is std::initializer_list<int>
template<typename T> // template with parameter
void f(T param); // declaration equivalent to x's declaration
f({ 11, 23, 9 }); // error! can't deduce type for T
2.3 decltype type deduction
decltype是用来获取某个变量名或者表达式的类型的,decltype类型推导的规则如下:
- decltype(lvalue expr of type T) = T&
- decltype(name T) = T
例如:
int x = 3;
decltype(x) a = x; // a's type is int
decltype((x)) b = a; // b's type is int&
vector<int> v;
decltype(v) c; //c's type is vector<int>
bool f(const Widget& w); // decltype(w) is const Widget&
// decltype(f) is bool(const Widget&)
struct Point {
int x, y; // decltype(Point::x) is int
}; // decltype(Point::y) is int
Widget w; // decltype(w) is Widget
if (f(w)) // decltype(f(w)) is bool
3. std::move and std::forward
std::move unconditionally casts its argument to an rvalue, while
std::forward performs this cast only if a particular condition is fulfilled. --<<Effective Modern C++>> item 23 Scott Meyers
C++11中的std::move和std::forward实际上是两个函数模板,它们在gcc 4.8.5中的实现如下(gcc4.9.2中也没有任何改动), 文件路径gcc-4.8.5/libstdc+±v3/include/bits/move.h,下面我们从它们的源码分析它们二者的功能:
std::move
/**
* @brief Convert a value to an rvalue.
* @param __t A thing of arbitrary type.
* @return The parameter cast to an rvalue-reference to allow moving it.
*/
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
从实现可以看出,move采用的是2.1.1广义引用的模板推导方式,
template<typename T>
void f(T&& param); // param is a universal reference
它的返回值是constexpr typename std::remove_reference<_Tp>::type&&,由2.1.1的推导规则:
- 当move(expr)中expr为类型为T的左值时,_Tp将会推导为T&, 那么 std::remove_reference<_Tp>::type的类型将会是去掉引用后的T, std::remove_reference<_Tp>::type&&的类型将会是T&&, 也就是右值引用
- 当move(expr)中expr为类型为T的右值时,_Tp将会推导为T, 那么 std::remove_reference<_Tp>::type的类型仍然是T, std::remove_reference<_Tp>::type&&的类型将会是T&&, 也就是右值引用
函数体也体现了这一点,不管f(expr)中的expr是类型为T的左值还是右值,函数都是将expr强转为类型为T的无名右值引用T&&(无名右值引用为右值),返回值是类型为T的右值引用T&&。
std::forward
/**
* @brief Forward an lvalue.
* @return The parameter cast to the specified type.
*
* This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
/**
* @brief Forward an rvalue.
* @return The parameter cast to the specified type.
*
* This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
" substituting _Tp is an lvalue reference type");
return static_cast<_Tp&&>(__t);
}
std::forward的实现采用了模板重载的方式,第一个是用来Forward an lvalue,第二个是用来Forward an rvalue。对于这两种重载,forward an lvalue通常更常见些,它被用在完美转发中,如下是一个完美转发的示例(可以在这里运行下面的程序):
#include <iostream>
void runCode(int&& m) { std::cout << "rvalue ref" << std::endl; }
void runCode(int& m) { std::cout << "lvalue ref" << std::endl; }
void runCode(const int&& m) { std::cout << "const rvalue ref" << std::endl; }
void runCode(const int& m) { std::cout << "const lvalue ref" << std::endl; }
template <typename T>
void perfectForward(T&& t) {
runCode(std::forward<T>(t));
}
int main() {
int a;
int b;
const int c = 1;
const int d = 0;
perfectForward(a); // T's type is int&
perfectForward(std::move(b)); // T's type is int&&
perfectForward(c); // T's type is const int&
perfectForward(std::move(d)); // T's type is const int&&
}
因为runCode(std::forward(t))这里的t是一个具名对象,可以取地址,t是一个左值,所以始终会调用第一个重载版本。这里函数模板perfectForward中的广义引用起到了关键作用:
- perfectForward(a)这一句中,类型推断后T为int&,函数体为runCode(std::forward<int&>(a)), 根据引用折叠原则,forward内部实际为static_cast<int&>(a), a本身类型为int&,因而static_cast没有做任何事,原样返回a,最终调用runCode(int& m)的版本。
- perfectForward(std::move(b)); 这一句中,std::move(b)类型为右值int&&, 类型推断后T为int,函数体为runCode(std::forward(a)),forward内部实际为static_cast<int&&>(b),b的类型为int, 经过cast后类型为int&&, 最终调用runCode(int&& m)的版本。
从上面可以看出perfectForward(expr)在转发给runCode函数时,不管expr是左值还是右值,runCode(std::forward(t));中的std::forward(t)完美的保留了expr的类型信息,这就是完美转发的核心所在。
注意到
runCode(std::forward<T>(t));
这一句中通过指定模板参数的方法避免了类型推导,没有采用下面这种类型推导的方式:
runCode(std::forward(t));
这是为什么呢?首先看一下模板函数:
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
这是因为这里t是一个左值,如果采用std::forward(t),那么模板推导后__t的类型必然是左值引用,返回的也就是左值引用。也就是说不管perfectForward(expr)的expr是左值还是右值,最后调用的都是void runCode(int& m),做不到完美转发。现在不少编译器禁止采用std::forward(t)的形式来调用std::forward也是这个原因。
对于上面的例子,我们注意到我们始终调用的是用来Forward an lvalue的这一重载版本,那么什么时候调用第二个版本呢?为了便于区别,我们将第一个重载版本写作l_forward, 第二个重载版本写作r_forward,根据如下的示例程序(可以在这里运行这个程序),可以发现,r_forward在参数是右值时可以直接调用。
// Example program
#include <iostream>
#include <string>
/**
* @brief Forward an lvalue.
* @return The parameter cast to the specified type.
*
* This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
constexpr _Tp&&
l_forward(typename std::remove_reference<_Tp>::type& __t) noexcept {
return static_cast<_Tp&&>(__t);
}
/**
* @brief Forward an rvalue.
* @return The parameter cast to the specified type.
*
* This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
constexpr _Tp&&
r_forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
" substituting _Tp is an lvalue reference type");
return static_cast<_Tp&&>(__t);
}
class Widget {
};
int main()
{
Widget w;
Widget& m = l_forward<Widget&>(w);
Widget&& n = r_forward<Widget&&>(Widget());
Widget& s = l_forward<Widget&>(n);
}
4. Move Constructor
Move Constructor也叫移动构造,C++11中会默认为每个类都生成移动构造函数和移动赋值操作符。当一个类所包含的对象占用空间较大时,拷贝构造函数需要重新申请空间,构造对象。而移动构造的作用是避免调用拷贝构造这些操作。移动构造函数形参一般是如下的右值引用,其声明如下所示:
Foo(Foo&& rhs);
调用移动构造函数一般采用的形式:
Foo b = std::move(a);
下面是一个移动构造的例子,可以在这里运行下面的程序:
// Example program
#include <iostream>
#include <string>
class BigObject {
public:
BigObject(int v) : m_value(v) {std::cout << "BigObject Default Constructor" << std::endl;}
virtual ~BigObject() {std::cout << "BigObject Destructor" << std::endl;}
void print() {std::cout << &m_value << std::endl;}
int getValue() const { return m_value; }
private:
int m_value;
};
class Foo {
public:
Foo() : m_ptrBig(new BigObject(3)){ std::cout << "Default Constructor" << std::endl;}
Foo(const Foo& rhs) : m_ptrBig(new BigObject(rhs.m_ptrBig->getValue())) {std::cout << "Copy Constructor" << std::endl;}
Foo(Foo&& rhs) : m_ptrBig(rhs.m_ptrBig) {rhs.m_ptrBig = nullptr; std::cout << "Move Constructor" << std::endl;}
virtual ~Foo() {delete m_ptrBig; std::cout << "Destructor" << std::endl;}
void print() {m_ptrBig->print();}
private:
BigObject* m_ptrBig;
};
int main()
{
Foo a;
a.print(); // print address of stored int
Foo b = std::move(a);
b.print(); // print address of stored int
}
运行结果为:
BigObject Default Constructor
Default Constructor
0x3945278
Move Constructor
0x3945278
BigObject Destructor
Destructor
Destructor
从上面的示例我们可以发现:
- 对于含有Big Object对象的类,如Foo,通过调用其移动构造函数,可以减少Big Object的构建;
- 移动构造函数内部不一定要调用std::move,使用移动构造函数构造对象需要显式调用std::move
5. Lambda expressions
lambda表达式的一般形式为:
[capture](parameters) mutable ->return-type { statement}
- capture:捕获列表,捕获父作用域的变量,[=]表示按值捕获所有,[&]表示按引用捕获,[=,&a,&b]表示按照引用传递的方式捕获a,b,按值传递的方式捕获所有其它变量。值得注意的时,引用捕获时是对变量的引用,lambda函数调用时始终使用的是变量最新的值,按值捕获时,改值在lambda函数定义时就已经决定了;
- parameters: 和一般函数的参数列表类似,如果参数列表为空,括号()也可以省略;
- mutable:mutable修饰符,默认情况下,lambda函数是const函数;使用mutable时参数列表不可省略;
- return-type: 返回类型,一般情况下可省略,由编译器进行推导。
lambda使用起来非常方便,leetcode上的这道题用lambda函数的方式解决就非常简洁明了: