C++ 11中的一些典型概念与分析

相比传统的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函数的方式解决就非常简洁明了:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/gigglesun/article/details/82926628