開發者都應該使用的10個C++11特性

auto編程

    在C++11以前,auto關鍵字用來指定存儲期。在新標準中,它的功能變爲類型推斷。auto如今成了一個類型的佔位符,通知編譯器去根據初始化代碼推斷所聲明變量的真實類型。各類做用域內聲明變量均可以用到它。例如,名空間中,程序塊中,或是for循環的初始化語句中。數組

auto i = 42;        // i is an int
auto l = 42LL;      // l is an long long
auto p = new foo(); // p is a foo*

使用auto一般意味着更短的代碼(除非你所用類型是int,它會比auto少一個字母)。試想一下當你遍歷STL容器時須要聲明的那些迭代器(iterator)。如今不須要去聲明那些typedef就能夠獲得簡潔的代碼了。ide

std::map<std::string, std::vector<int>> map;
for(auto it = begin(map); it != end(map); ++it) {}

須要注意的是,auto不能用來聲明函數的返回值。但若是函數有一個尾隨的返回類型時,auto是能夠出如今函數聲明中返回值位置。這種狀況下,auto並非告訴編譯器去推斷返回類型,而是指引編譯器去函數的末端尋找返回值類型。在下面這個例子中,函數的返回值類型就是operator+操做符做用在T一、T2類型變量上的返回值類型。函數式編程

template <typename T1, typename T2>
auto compose(T1 t1, T2 t2) -> decltype(t1 + t2)
{
   return t1+t2;
}
auto v = compose(2, 3.14); // v's type is double

 nullptr函數

    之前都是用0來表示空指針的,但因爲0能夠被隱式類型轉換爲整形,這就會存在一些問題。關鍵字nullptr是std::nullptr_t類型的值,用來指代空指針。nullptr和任何指針類型以及類成員指針類型的空值之間能夠發生隱式類型轉換,一樣也能夠隱式轉換爲bool型(取值爲false)。可是不存在到整形的隱式類型轉換oop

void foo(int* p) {}
void bar(std::shared_ptr<int> p) {}
int* p1 = NULL;
int* p2 = nullptr;   
if(p1 == p2) {}
foo(nullptr);
bar(nullptr);
bool f = nullptr;
int i = nullptr; // error: A native nullptr can only be converted to bool or, using reinterpret_cast, to an integral type

爲了向前兼容,0仍然是個合法的空指針值。學習

Range-based for loops (基於範圍的for循環)this

    爲了在遍歷容器時支持」foreach」用法,C++11擴展了for語句的語法。用這個新的寫法,能夠遍歷C類型的數組、初始化列表以及任何重載了非成員的begin()和end()函數的類型。若是你只是想對集合或數組的每一個元素作一些操做,而不關心下標、迭代器位置或者元素個數,那麼這種foreach的for循環將會很是有用。lua

std::map<std::string, std::vector<int>> map;
std::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
map["one"] = v;
for(const auto& kvp : map) 
{
  std::cout << kvp.first << std::endl;
  for(auto v : kvp.second)
  {
     std::cout << v << std::endl;
  }
}
int arr[] = {1,2,3,4,5};
for(int& e : arr) 
{
  e = e*e;
}

override和finalspa

    我總以爲 C++中虛函數的設計不好勁,由於時至今日仍然沒有一個強制的機制來標識虛函數會在派生類裏被改寫。vitual關鍵字是可選的,這使得閱讀代碼變得很費勁。由於可能須要追溯到繼承體系的源頭才能肯定某個方法是不是虛函數。爲了增長可讀性,我老是在派生類裏也寫上virtual關鍵字,而且也鼓勵你們都這麼作。即便這樣,仍然會產生一些微妙的錯誤。看下面這個例子:

class A 
{
public:
   virtual void f(short) {std::cout << "A::f" << std::endl;}
};
class B : public A
{
public:
   virtual void f(int) {std::cout << "B::f" << std::endl;}
};

B::f 按理應當重寫 A::f。然而兩者的聲明是不一樣的,一個參數是short,另外一個是int。所以B::f只是擁有一樣名字的另外一個函數(重載)而不是重寫。當你經過A類型的指針調用f()可能會指望打印出B::f,但實際上則會打出 f(int)而不是f(short) 。另外一個很微妙的錯誤狀況:參數相同,可是基類的函數是const的,派生類的函數卻不是。

class A 
{
public:
   virtual void f(int) const {std::cout << "A::f " << std::endl;}
};
class B : public A
{
public:
   virtual void f(int) {std::cout << "B::f" << std::endl;}
};

一樣,這兩個函數是重載而不是重寫。幸運的是,如今有一種方式能描述你的意圖。新標準加入了兩個新的標識符(不是關鍵字)

override,表示函數必須重寫基類中的虛函數,若是派生類沒有重寫到將編譯報錯。

final,表示派生類不該當重寫這個虛函數,若是派生類重寫了基類的虛函數將編譯報錯。

class A
{
public:
    virtual void f(short) { std::cout << "A::f" << std::endl; }
    virtual void g(int) final { std::cout << "A::g" << std::endl; }
};
class B : public A
{
public:
    virtual void f(short)  override  { std::cout << "B::f" << std::endl; }
    //virtual void g(int) { std::cout << "A::g" << std::endl; } // error C3248: 「main::A::g」:  聲明爲「final」的函數沒法被「main::B::g」重寫
    virtual void g(float) { std::cout << "A::g" << std::endl; } // 重載
};

Strongly-typed enums 強類型枚舉

    傳統的C++枚舉類型存在一些缺陷:它們會將枚舉常量暴露在外層做用域中(這可能致使名字衝突,若是同一個做用域中存在兩個不一樣的枚舉類型,可是具備相同的枚舉常量就會衝突),並且它們會被隱式轉換爲整形,沒法擁有特定的用戶定義類型。

在C++11中經過引入了一個稱爲強類型枚舉的新類型,修正了這種狀況。強類型枚舉由關鍵字enum class標識。它不會將枚舉常量暴露到外層做用域中,也不會隱式轉換爲整形,而且擁有用戶指定的特定類型(傳統枚舉也增長了這個性質)。

enum class Options {None, One, All};
Options o = Options::All;

Smart Pointers 智能指針

    已經有成千上萬的文章討論這個問題了,因此我只想說:如今能使用的,帶引用計數,而且能自動釋放內存的智能指針包括如下幾種:

unique_ptr: 若是內存資源的全部權不須要共享,就應當使用這個(它沒有拷貝構造函數),可是它能夠轉讓給另外一個unique_ptr(存在move構造函數)。

shared_ptr:  若是內存資源須要共享,那麼使用這個(因此叫這個名字)。

weak_ptr: 持有被shared_ptr所管理對象的引用,可是不會改變引用計數值。它被用來打破依賴循環(想象在一個tree結構中,父節點經過一個共享全部權的引用(chared_ptr)引用子節點,同時子節點又必須持有父節點的引用。若是這第二個引用也共享全部權,就會致使一個循環,最終兩個節點內存都沒法釋放)。

另外一方面,auto_ptr已經被廢棄,不會再使用了。

何時使用unique_ptr,何時使用shared_ptr取決於對全部權的需求,我建議閱讀如下的討論:http://stackoverflow.com/questions/15648844/using-smart-pointers-for-class-members

std::unique_ptr<int> p1(new int(42));
std::unique_ptr<int> p2 = std::move(p1); // 移交unique指針
auto sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;
{
	auto sp = wp.lock(); // 提高shared_ptr
	std::cout << *sp << std::endl;
}
sp.reset();

if (wp.expired())
	std::cout << "expired" << std::endl;

若是你試圖鎖定(lock)一個過時(指被弱引用對象已經被釋放)的weak_ptr,那你將得到一個空的shared_ptr.

Lambdas

    匿名函數(也叫lambda)已經加入到C++中,並很快異軍突起。這個從函數式編程中借來的強大特性,使不少其餘特性以及類庫得以實現。你能夠在任何使用函數對象或者函子(functor)或std::function的地方使用lambda。你能夠從這裏(http://msdn.microsoft.com/en-us/library/dd293603.aspx)找到語法說明。

std::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
std::for_each(std::begin(v), std::end(v), [](int n) {std::cout << n << std::endl;});
auto is_odd = [](int n) {return n%2==1;};
auto pos = std::find_if(std::begin(v), std::end(v), is_odd);
if(pos != std::end(v))
    std::cout << *pos << std::endl;

更復雜的是遞歸lambda。考慮一個實現Fibonacci函數的lambda。若是你試圖用auto來聲明,就會獲得一個編譯錯誤。

auto fib = [&fib](int n) {return n < 2 ? 1 : fib(n-1) + fib(n-2);};
error C3533: 'auto &': a parameter cannot have a type that contains 'auto'
error C3531: 'fib': a symbol whose type contains 'auto' must have an initializer
error C3536: 'fib': cannot be used before it is initialized
error C2064: term does not evaluate to a function taking 1 arguments

問題出在auto意味着對象類型由初始表達式決定,然而初始表達式又包含了對其自身的引用,所以要求先知道它的類型,這就致使了無窮遞歸。解決問題的關鍵就是打破這種循環依賴,用std::function顯式的指定函數類型:

std::function<int(int)> lfib = [&lfib](int n) {return n < 2 ? 1 : lfib(n-1) + lfib(n-2);};

 非成員begin()和end()

    也許你注意到了,我在前面的例子中已經用到了非成員begin()和end()函數。他們是新加入標準庫的,除了能提升了代碼一致性,還有助於更多地使用泛型編程。它們和全部的STL容器兼容。更重要的是,他們是可重載的。因此它們能夠被擴展到支持任何類型。對C類型數組的重載已經包含在標準庫中了。咱們還用上一個例子中的代碼來講明,在這個例子中我打印了一個數組而後查找它的第一個偶數元素。若是std::vector被替換成C類型數組。代碼可能看起來是這樣的:

int arr[] = {1,2,3};
std::for_each(&arr[0], &arr[0]+sizeof(arr)/sizeof(arr[0]), [](int n) {std::cout << n << std::endl;});
auto is_odd = [](int n) {return n%2==1;};
auto begin = &arr[0];
auto end = &arr[0]+sizeof(arr)/sizeof(arr[0]);
auto pos = std::find_if(begin, end, is_odd);
if(pos != end)
    std::cout << *pos << std::endl;

若是使用非成員的begin()和end()來實現,就會是如下這樣的:

int arr[] = {1,2,3};
std::for_each(std::begin(arr), std::end(arr), [](int n) {std::cout << n << std::endl;});
auto is_odd = [](int n) {return n%2==1;};
auto pos = std::find_if(std::begin(arr), std::end(arr), is_odd);
if(pos != std::end(arr))
    std::cout << *pos << std::endl;

這基本上和使用std::vecto的代碼是徹底同樣的。這就意味着咱們能夠寫一個泛型函數處理全部支持begin()和end()的類型。

template <typename Iterator>
void bar(Iterator begin, Iterator end)
{
    std::for_each(begin, end, [](int n) {std::cout << n << std::endl;});
    auto is_odd = [](int n) {return n%2==1;};
    auto pos = std::find_if(begin, end, is_odd);
    if(pos != end)
        std::cout << *pos << std::endl;
}
template <typename C>
void foo(C c)
{
    bar(std::begin(c), std::end(c));
}
template <typename T, size_t N>
void foo(T(&arr)[N])
{
    bar(std::begin(arr), std::end(arr));
}
int arr[] = {1,2,3};
foo(arr);
std::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
foo(v);

static_assert和 type traits

    static_assert提供一個編譯時的斷言檢查。若是斷言爲真,什麼也不會發生。若是斷言爲假,編譯器會打印一個特殊的錯誤信息。

template <typename T, size_t Size>
class Vector
{
   static_assert(Size < 3, "Size is too small");
   T _points[Size];
};
int main()
{
   Vector<int, 16> a1;
   Vector<double, 2> a2;
   return 0;
}

error C2338: Size is too small
see reference to class template instantiation 'Vector<T,Size>' being compiled
   with
   [
      T=double,
      Size=2
   ]

static_assert和type traits一塊兒使用能發揮更大的威力。type traits是一些class,在編譯時提供關於類型的信息。在頭文件<type_traits>中能夠找到它們。這個頭文件中有好幾種class: helper class,用來產生編譯時常量。type traits class,用來在編譯時獲取類型信息,還有就是type transformation class,他們能夠將已存在的類型變換爲新的類型。

下面這段代碼本來指望只作用於整數類型。

template <typename T1, typename T2>
auto add(T1 t1, T2 t2)
{
    return t1 + t2;
}

可是若是有人寫出以下代碼,編譯器並不會報錯

std::cout << add(1, 3.14) << std::endl;
std::cout << add("one", 2) << std::endl;

程序會打印出4.14和」e」。可是若是咱們加上編譯時斷言,那麼以上兩行將產生編譯錯誤。

template <typename T1, typename T2>
auto add(T1 t1, T2 t2)
{
   static_assert(std::is_integral<T1>::value, "Type T1 must be integral");
   static_assert(std::is_integral<T2>::value, "Type T2 must be integral");
   return t1 + t2;
}

error C2338: Type T2 must be integral
see reference to function template instantiation 'T2 add<int,double>(T1,T2)' being compiled
   with
   [
      T2=double,
      T1=int
   ]
error C2338: Type T1 must be integral
see reference to function template instantiation 'T1 add<const char*,int>(T1,T2)' being compiled
   with
   [
      T1=const char *,
      T2=int
   ]

Move semantics (Move語義)

    這是C++11中所涵蓋的另外一個重要話題。就這個話題能夠寫出一系列文章,僅用一個段落來講明顯然是不夠的。所以在這裏我不會過多的深刻細節,若是你還不是很熟悉這個話題,我鼓勵你去閱讀更多地資料。C++11加入了右值引用(value reference)的概念(用&&標識),用來區分對左值和右值的引用。左值就是一個有名字的對象,而右值則是一個無名對象(臨時對象)。move語義容許修改右值(之前右值被看做是不可修改的,等同於const T&類型)。C++的class或者struct之前都有一些隱含的成員函數:默認構造函數(僅當沒有顯示定義任何其餘構造函數時才存在),拷貝構造函數,析構函數還有拷貝賦值操做符。拷貝構造函數和拷貝賦值操做符提供bit-wise的拷貝(淺拷貝),也就是逐個bit拷貝對象。也就是說,若是你有一個類包含指向其餘對象的指針,拷貝時只會拷貝指針的值而不會管指向的對象。在某些狀況下這種作法是沒問題的,但在不少狀況下,實際上你須要的是深拷貝,也就是說你但願拷貝指針所指向的對象。而不是拷貝指針的值。這種狀況下,你須要顯示地提供拷貝構造函數與拷貝賦值操做符來進行深拷貝。若是你用來初始化或拷貝的源對象是個右值(臨時對象)會怎麼樣呢?你仍然須要拷貝它的值,但隨後很快右值就會被釋放。這意味着產生了額外的操做開銷,包括本來並不須要的空間分配以及內存拷貝。如今說說move constructor和move assignment operator。這兩個函數接收T&&類型的參數,也就是一個右值。在這種狀況下,它們能夠修改右值對象,例如「偷走」它們內部指針所指向的對象。舉個例子,一個容器的實現(例如vector或者queue)可能包含一個指向元素數組的指針。當用一個臨時對象初始化一個對象時,咱們不須要分配另外一個數組,從臨時對象中把值複製過來,而後在臨時對象析構時釋放它的內存。咱們只須要將指向數組內存的指針值複製過來,由此節約了一次內存分配,一次元數組的複製以及後來的內存釋放。如下代碼實現了一個簡易的buffer。這個buffer有一個成員記錄buffer名稱(爲了便於如下的說明),一個指針(封裝在unique_ptr中)指向元素爲T類型的數組,還有一個記錄數組長度的變量。

template <typename T>
class Buffer 
{
   std::string          _name;
   size_t               _size;
   std::unique_ptr<T[]> _buffer;
 
public:
   // default constructor
   Buffer():
      _size(16),
      _buffer(new T[16])
   {}
 
   // constructor
   Buffer(const std::string& name, size_t size):
      _name(name),
      _size(size),
      _buffer(new T[size])
   {}
 
   // copy constructor
   Buffer(const Buffer& copy):
      _name(copy._name),
      _size(copy._size),
      _buffer(new T[copy._size])
   {
      T* source = copy._buffer.get();
      T* dest = _buffer.get();
      std::copy(source, source + copy._size, dest);
   }
 
   // copy assignment operator
   Buffer& operator=(const Buffer& copy)
   {
      if(this != ©)
      {
         _name = copy._name;
 
         if(_size != copy._size)
         {
            _buffer = nullptr;
            _size = copy._size;
            _buffer = _size > 0 > new T[_size] : nullptr;
         }
 
         T* source = copy._buffer.get();
         T* dest = _buffer.get();
         std::copy(source, source + copy._size, dest);
      }
 
      return *this;
   }
 
   // move constructor
   Buffer(Buffer&& temp):
      _name(std::move(temp._name)),
      _size(temp._size),
      _buffer(std::move(temp._buffer))
   {
      temp._buffer = nullptr;
      temp._size = 0;
   }
 
   // move assignment operator
   Buffer& operator=(Buffer&& temp)
   {
      assert(this != &temp); // assert if this is not a temporary
 
      _buffer = nullptr;
      _size = temp._size;
      _buffer = std::move(temp._buffer);
 
      _name = std::move(temp._name);
 
      temp._buffer = nullptr;
      temp._size = 0;
 
      return *this;
   }
};
 
template <typename T>
Buffer<T> getBuffer(const std::string& name) 
{
   Buffer<T> b(name, 128);
   return b;
}
int main()
{
   Buffer<int> b1;
   Buffer<int> b2("buf2", 64);
   Buffer<int> b3 = b2;
   Buffer<int> b4 = getBuffer<int>("buf4");
   b1 = getBuffer<int>("buf5");
   return 0;
}

默認的copy constructor以及copy assignment operator你們應該很熟悉了。C++11中新增的是move constructor以及move assignment operator,這兩個函數根據上文所描述的move語義實現。若是你運行這段代碼,你就會發現b4構造時,move constructor會被調用。一樣,對b1賦值時,move assignment operator會被調用。緣由就在於getBuffer()的返回值是一個臨時對象——也就是右值。你也許注意到了,move constuctor中當咱們初始化變量name和指向buffer的指針時,咱們使用了std::move。name其實是一個string,std::string實現了move語義。std::unique_ptr也同樣。可是若是咱們寫_name(temp._name),那麼copy constructor將會被調用。不過對於_buffer來講不能這麼寫,由於std::unique_ptr沒有copy constructor。但爲何std::string的move constructor此時沒有被調到呢?這是由於雖然咱們使用一個右值調用了Buffer的move constructor,但在這個構造函數內,它其實是個左值。爲何?由於它是有名字的——「temp」。一個有名字的對象就是左值。爲了再把它變爲右值(以便調用move constructor)必須使用std::move。這個函數僅僅是把一個左值引用變爲一個右值引用。更新:雖然這個例子是爲了說明如何實現move constructor以及move assignment operator,但具體的實現方式並非惟一的。某同窗提供了另外一種可能的實現。爲了方便查看,我把它也列在下面:

template <typename T>
class Buffer
{
   std::string          _name;
   size_t               _size;
   std::unique_ptr<T[]> _buffer;
 
public:
   // constructor
   Buffer(const std::string& name = "", size_t size = 16):
      _name(name),
      _size(size),
      _buffer(size? new T[size] : nullptr)
   {}
 
   // copy constructor
   Buffer(const Buffer& copy):
      _name(copy._name),
      _size(copy._size),
      _buffer(copy._size? new T[copy._size] : nullptr)
   {
      T* source = copy._buffer.get();
      T* dest = _buffer.get();
      std::copy(source, source + copy._size, dest);
   }
 
   // copy assignment operator
   Buffer& operator=(Buffer copy)
   {
       swap(*this, copy);
       return *this;
   }
 
   // move constructor
   Buffer(Buffer&& temp):Buffer()
   {
      swap(*this, temp);
   }
 
   friend void swap(Buffer& first, Buffer& second) noexcept
   {
       using std::swap;
       swap(first._name  , second._name);
       swap(first._size  , second._size);
       swap(first._buffer, second._buffer);
   }
};

 結論

    關於C++11還有不少要說的。本文只是各類入門介紹中的一個。本文展現了一系列C++開發者應當使用的核心語言特性與標準庫函數。然而我建議你能更加深刻地學習,至少也要再看看本文所介紹的特性中的部分。

相關文章
相關標籤/搜索