您好,登錄后才能下訂單哦!
這篇文章主要介紹“c++右值引用和移動構造實例分析”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“c++右值引用和移動構造實例分析”文章能幫助大家解決問題。
C++11 新特性分析
auto
現在是一種類型占位符,它會告訴編譯器,應該從初始化式中推斷出變量的實際類型。當你想在不同的作用域中(例如,命名空間、函數內、for循環中中的初始化式)聲明變量的時候,auto可以在這些場合使用。
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這種只有一個單詞的)。當你想要遍歷STL容器中元素的時候,想一想你會怎么寫迭代器代碼,老式的方法是用很多typedef來做,而auto則會大大簡化這個過程。
std::map<std::string, std::vector<int>> map;
for(auto it = begin(map); it != end(map); ++it)
{
}
你應該注意到,auto并不能作為函數的返回類型,但是你能用auto去代替函數的返回類型,當然,在這種情況下,函數必須有返回值才可以。auto不會告訴編譯器去推斷返回值的實際類型,它會通知編譯器在函數的末段去尋找返回值類型。在下面的那個例子中,函數返回值的構成是由T1類型和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曾經是空指針的值,這種方式有一些弊端,因為它可以被隱式轉換成整型變量。nullptr關鍵字代表值類型std::nullptr_t,在語義上可以被理解為空指針。nullptr可被隱式轉換成任何類型的空指針,以及成員函數指針和成員變量指針,而且也可以轉換為bool(值為false),但是隱式轉換到整型變量的情況不再存在了。
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仍可作為空指針的值來使用。
基于區間的循環
C++11加強了for語句的功能,以更好的支持用于遍歷集合的foreach范式。在新的形式中,用戶可以使用for去迭代遍歷C風格的數組、初始化列表,以及所有非成員begin()和end()被重載的容器。
當你僅僅想獲取集合/數組中的元素來做一些事情,而不關注索引值、迭代器或者元素本身的時候,這種for的形式非常有用。
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和final
虛函數在C++中會引起很多問題,因為沒有一個強制的機制來標識虛函數在派生類中被重寫了。virtual關鍵字并不是強制性的,這給代碼的閱讀增加了一些困難,因為你可能不得不去看繼承關系的最頂層以確認這個方法是不是虛方法。我自己經常鼓勵開發者在派生類中使用virtual關鍵字,我自己也是這么做的,這可以讓代碼更易讀。然而,有一些不明顯的錯誤仍然會出現,下面這段代碼就是個例子。
class B
{
public:
virtual void f(short) {std::cout << "B::f" << std::endl;}
};
class D : public B
{
public:
virtual void f(int) {std::cout << "D::f" << std::endl;}
};
D::f本應該重寫B::f,但是這兩個函數的簽名并不相同,一個參數是short,另一個則是int,因此,B::f僅僅是另外一個和D::f命名相同的函數,是重載而不是重寫。你有可能會通過B類型的指針調用f(),并且期盼輸出D::f的結果,但是打印出來的結果卻是B::f。
這里還有另外一個不明顯的錯誤:參數是相同的,但是在基類中的函數是const成員函數,而在派生類中則不是。
class B
{
public:
virtual void f(int) const {std::cout << "B::f " << std::endl;}
};
class D : public B
{
public:
virtual void f(int) {std::cout << "D::f" << std::endl;}
};
又一次,這兩個函數的關系是重載而非重寫,因此,如果你想通過B類型的指針來調用f(),程序會打印出B::f,而不是D::f。
幸運的是,有一種方法可以來描述你的意圖,兩個新的、專門的標識符(不是關鍵字)添加進了C++11中:override,可以指定在基類中的虛函數應該被重寫;final,可以用來指定派生類中的函數不會重寫基類中的虛函數。第一個例子會變成:
class B
{
public:
virtual void f(short) {std::cout << "B::f" << std::endl;}
};
class D : public B
{
public:
virtual void f(int) override {std::cout << "D::f" << std::endl;}
};
這段代碼會觸發一個編譯錯誤(如果你使用override標識符嘗試第二個例子,也會得到相同的錯誤。):
'D::f': 有override標識符的函數并沒有重寫任何基類函數
另一方面,如果你想要一個函數永遠不能被重寫(順著繼承層次往下都不能被重寫),你可以把該函數標識為final,在基類中和派生類中都可以這么做。如果實在派生類中,你可以同時使用override和final標識符。
class B
{
public:
virtual void f(int) {std::cout << "B::f" << std::endl;}
};
class D : public B
{
public:
virtual void f(int) override final {std::cout << "D::f" << std::endl;}
};
class F : public D
{
public:
virtual void f(int) override {std::cout << "F::f" << std::endl;}
};
用final聲明的函數不能被F::f重寫。
強類型枚舉
“傳統”的C++枚舉類型有一些缺點:它會在一個代碼區間中拋出枚舉類型成員(如果在相同的代碼域中的兩個枚舉類型具有相同名字的枚舉成員,這會導致命名沖突),它們會被隱式轉換為整型,并且不可以指定枚舉的底層數據類型。
通過引入一種新的枚舉類型,這些問題在C++11中被解決了,這種新的枚舉類型叫做強類型枚舉。這種類型用enum class關鍵字來標識,它永遠不會在代碼域中拋出枚舉成員,也不會隱式的轉換為整形,同時還可以具有用戶指定的底層類型(這個特征也被加入了傳統枚舉類型中)。
enum class Options {None, One, All};
Options o = Options::All;
智能指針
智能指針的引用計數和內存自動釋放相關:
unique_ptr:當一塊內存的所有權并不是共享的時候(它并不具有拷貝構造函數),可以使用,但是,它可以被轉換為另外一個unique_ptr(具有移動構造函數)。
shared_ptr:當一塊內存的所有權可以被共享的時候,可以使用(這就是為什么它叫這個名)。
weak_ptr:具有一個shared_ptr管理的指向一個實體對象的引用,但是并沒有做任何引用計數的工作,它被用來打破循環引用關系(想象一個關系樹,父節點擁有指向子節點的引用(shared_ptr),但是子節點也必須持有指向父節點的引用;如果第二個引用也是一個獨立的引用,一個循環就產生了,這會導致任何對象都永遠無法釋放)。
換句話說,auto_ptr已經過時了,應該不再被使用了。
什么時候該使用unique_ptr,什么時候該使用shared_ptr,取決于程序對內存所有權的需求,我推薦你讀一讀這里的討論。
下面第一個例子演示了unique_ptr的用法,如果你想要把對象的控制權轉交給另一個unique_ptr,請使用std::move。在控制權交接后,讓出控制權的智能指針會變成null,如果調用get(),會返回nullptr。
void foo(int* p)
{
std::cout << *p << std::endl;
}
std::unique_ptr<int> p1(new int(42));
std::unique_ptr<int> p2 = std::move(p1); // transfer ownership
if(p1)
foo(p1.get());
(*p2)++;
if(p2)
foo(p2.get());
第二個例子演示了shared_ptr的用法。盡管語義不同,因為所有權是共享的,但用法都差不多。
void foo(int* p)
{
}
void bar(std::shared_ptr<int> p)
{
++(*p);
}
std::shared_ptr<int> p1(new int(42));
std::shared_ptr<int> p2 = p1;
bar(p1);
foo(p2.get());
第一個聲明等價于這個。
auto p3 = std::make_shared<int>(42);
make_shared是一個非成員函數,具有給共享對象分配內存,并且只分配一次內存的優點,和顯式通過構造函數初始化的shared_ptr相比較,后者需要至少兩次分配內存。這些額外的開銷有可能會導致內存溢出的問題,在下一個例子中,如果seed()拋出一個異常,則表示發生了內存溢出。
void foo(std::shared_ptr<int> p, int init)
{
*p = init;
}
foo(std::shared_ptr<int>(new int(42)), seed());
如果使用make_shared,則可以避開類似問題。第三個例子展示了weak_ptr的用法,注意,你必須通過調用lock()來獲取shared_ptr中指向對象的引用,以此來訪問對象。
auto p = std::make_shared<int>(42);
std::weak_ptr<int> wp = p;
{
auto sp = wp.lock();
std::cout << *sp << std::endl;
}
p.reset();
if(wp.expired())
std::cout << "expired" << std::endl;
如果你試圖在一個已經過期的weak_ptr上調用lock(被弱引用的對象已經被釋放了),你會得到一個空的shared_ptr。
Lambdas表達式
匿名的方法,也叫做lambda表達式,被加進了C++11標準里,并且立刻得到了開發者們的重視。這是一個從函數式語言中借鑒來的,非常強大的特征,它讓一些其他的特征和強大的庫得以實現。在任何函數對象、函數、std::function中出現的地方,你都可以用lambda表達式:
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表達式。想象一個代表斐波那契函數的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中的東西,提升了語言的標準性和一致性,也使更多的泛型編程變成了可能,它們和所有的STL容器都是兼容的,但卻不僅僅是簡單的重載,因此你可以隨意擴展begin()和end(),以便兼容任何類型,針對C類型數組的重載也一樣是支持的。
讓我們舉一個前面寫過的例子,在這個例子中,我試圖打印輸出一個vector,并且找到它的第一個奇數值的元素。如果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::vector那段代碼一樣,這意味著我們可以為所有支持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宏和類型萃取器
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會變得更加有用,這些是一系列可以在編譯期提供額外信息的類,它們被封裝在了頭文件里面,在這個頭文件里,有若干分類:用來創建編譯期常量的helper類,用來編譯期獲取類型信息的類型萃取類,為了可以把現存類型轉換為新類型的類型轉換類。
在下面那個例子里,add函數被設計成只能處理基本類型。
template <typename T1, typename T2>
auto add(T1 t1, T2 t2) -> decltype(t1 + 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) -> decltype(t1 + 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
]
移動語義
這又是一個很重要,并且涉及到很多C++11技術特征的話題,關于這個話題不僅僅能寫一段,更能寫一系列文章。
為了區分指向左值的引用和指向右值的引用,C++11引入了右值引用(用&&來表示)的概念。左值是指一個有名字的對象,而右值則是一個沒有名字的對象(臨時對象)。移動語義允許修改右值(之前考慮到它的不可改變性,因此和const T& types的概念有些混淆)。
一個C++類/結構體有一些隱式成員函數:默認構造函數(當且僅當另外一個構造函數沒有被顯式的定義),拷貝構造函數,一個析構函數,以及一個拷貝賦值操作符。拷貝構造函數和拷貝賦值操作符一般會執行按位拷貝(或者淺拷貝),例如,逐一按位拷貝變量。這意味著如果你有一個包含指向某個對象的指針的類,它們只會把指針的地址進行拷貝,并不會拷貝指針指向的對象。這在某些情況下是可以的,但是對于絕大多數情況,你需要的是深拷貝,也就是對指針指向的對象進行拷貝,而不是指針本身的值,在這種情況下你不得不顯式的寫一個拷貝構造函數和拷貝賦值操作符來執行深拷貝。
那么,如果你想要初始化或者復制的源數據是個右值類型(臨時的)會怎么樣?你仍然不得不拷貝它的值,但是很快,這個右值就會消失,這意味著一些操作的開銷,包括分配內存以及最后拷貝數據,這些都是不必要的。
我們引入了移動構造函數和移動賦值操作符,這兩個特殊的函數接受一個T&&類型的右值參數,這兩個函數可以修改對象,類似于把引用指向的對象“偷”來。舉一個例子,一個容器的具體實現(例如vector或者queue)可能會包含一個指向數組元素的指針,我們可以為這些元素分配另一個數組空間,從臨時空間中拷貝數據,然后當臨時數據失效的時候再刪除這段內存,我們也可以直接用這個臨時的數據來實例化,我們只是拷貝指向數組元素的指針地址,于是,這節省了一次分配內存的開銷,拷貝一系列元素并且稍后釋放掉的開銷。
下面這個例子展示了一個虛擬緩沖區的實現,這段緩沖區由一個名字標識(只是為了能更好的解釋),有一個指針(用std::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;
}
默認拷貝構造函數和復制賦值操作符應該看起來很類似,對于C++11標準來說,新的東西是根據移動語義設計的移動構造函數和移動賦值操作符。如果你運行這段代碼,你會看到,當b4被構造的時候,調用了移動構造函數。而當b1被分配一個值的時候,移動賦值操作符被調用了,原因則是getBuffer()返回的值是一個臨時的右值。
你可能注意到了一個細節,當初始化name變量和指向buffer的指針的時候,我們在移動構造函數中使用了std::move。name變量是一個字符串類型,std::string支持移動語義,unique_ptr也是一樣的,然而,如果我們使用_name(temp._name),復制構造函數將會被調用,但對于_buffer來說,這卻是不可能的,因為std::unique_ptr并沒有拷貝構造函數,但是為什么std::string的移動構造函數在這種情況下沒有被調用?因為即使為Buffer調用移動構造函數的對象是一個右值類型,在構造函數的內部卻實際是個左值類型,為什么?因為他有一個名字temp,而一個有名字的對象是左值類型。為了讓它再一次變成右值類型(也為了可以恰當的調用移動構造函數),我們必須使用std::move。這個函數的作用只是把一個左值類型的引用轉換成右值類型引用。
更新:雖然這個例子的目的是展示下如何實現移動構造函數和移動賦值操作符,但實現的具體細節可能會有所不同.
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++右值引用和移動構造實例分析”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。