C++ 右值語義相關總結
在現(xiàn)代C++的眾多特性中,右值語義(std::move和std::forward)大概是最神奇也最難懂的特性之一了。本文簡要介紹了現(xiàn)代C++中右值語義特性的原理和使用。
1 什么是左值,什么是右值?
int a = 0; // a是左值,0是右值 int b = rand(); // b是左值,rand()是右值
直觀理解:左值在等號左邊,右值在等號右邊
深入理解:左值有名稱,可根據(jù)左值獲取其內存地址,而右值沒有名稱,不能根據(jù)右值獲取地址。
2 引用疊加規(guī)則
左值引用A&和右值引用A&&可相互疊加, 疊加規(guī)則如下:
A& + A& = A& A& + A&& = A& A&& + A& = A& A&& + A&& = A&&
舉例說明,在模板函數(shù)void foo(T&& x)中:
如果T是int&類型, T&&為int&,x為左值語義
如果T是int&&類型, T&&為int&&, x為右值語義
也就是說,不管輸入?yún)?shù)x為左值還是右值,都能傳入函數(shù)foo。區(qū)別在于兩種情況下,編譯器推導出模板參數(shù)T的類型不一樣。
3 std::move
3.1 What?
在C++11中引入了std::move函數(shù),用于實現(xiàn)移動語義。它用于將臨時變量(也有可能是左值)的內容直接移動給被賦值的左值對象。
3.2 Why?
知道了std::move是干什么的,他能給我們的搬磚工作帶來哪些好處呢? 舉例說明:
如果類X包含一個指向某資源的指針,在左值語義下,類X的復制構造函數(shù)定義如下:
X::X()
{
// 申請資源(指針表示)
}
X::X(const X& other)
{
// ...
// 銷毀資源
// 克隆other中的資源
// ...
}
X::~X()
{
// 銷毀資源
}
假設應用代碼如下。其中,對象tmp被賦給a之后,便不再使用。
X tmp; // ...經(jīng)過一系列初始化... X a = tmp;
在上面的代碼中,執(zhí)行步驟:
- 先執(zhí)行一次默認構造函數(shù)(默認構造tmp對象)
- 再執(zhí)行一次復制構造函數(shù)(復制構造a對象)
- 退出作用域時執(zhí)行析構函數(shù)(析構tmp和a對象)
從資源的視角來看,上述代碼中共執(zhí)行了2次資源申請和3次資源釋放。
那么問題來了,既然對象tmp只是一個臨時對象,在執(zhí)行X a = tmp;時,對象a能否將tmp的資源'偷'過來,直接為我所用,而不影響原來的功能? 答案是可以。
X::X(const X& other)
{
// 使用std::swap交換this和other的資源
}
通過'偷'對象tmp的資源,減少了資源申請和釋放的開銷。而std::swap交換指針代價極小,可忽略不計。
3.3 How?
到現(xiàn)在為止,我們明白了std::move將要達到的效果,那么它究竟是怎么實現(xiàn)的呢?
template<class T>
typename remove_reference<T>::type&&
std::move(T&& a) noexcept
{
typedef typename remove_reference<T>::type&& RvalRef;
return static_cast<RvalRef>(a);
}
不管輸入?yún)?shù)為左值還是右值,都被remove_reference去掉其引用屬性,RvalRef為右值類型,最終返回類型為右值引用。
3.4 Example
在實際使用中,一般將臨時變量作為std::move的輸入?yún)?shù),并將返回值傳入接受右值類型的函數(shù)中,方便其'偷取'臨時變量中的資源。需要注意的是,臨時變量被'偷'了之后,便不能對其進行讀寫,否則會產(chǎn)生未定義行為。
#include <utility>
#include <iostream>
#include <string>
#include <vector>
void foo(const std::string& n)
{
std::cout << "lvalue" << std::endl;
}
void foo(std::string&& n)
{
std::cout << "rvalue" << std::endl;
}
void bar()
{
foo("hello"); // rvalue
std::string a = "world";
foo(a); // lvalue
foo(std::move(a)); // rvalue
}
int main()
{
std::vector<std::string> a = {"hello", "world"};
std::vector<std::string> b;
b.push_back("hello"); // 開銷:string復制構造
b.push_back(std::move(a[1])); // 開銷:string移動構造(將臨時變量a[1]中的指針偷過來)
std::cout << "bsize: " << b.size() << std::endl;
for (std::string& x: b)
std::cout << x << std::endl;
bar();
return 0;
}
4 std::forward
4.1 What?
std::forward用于實現(xiàn)完美轉發(fā)。那么什么是完美轉發(fā)呢?完美轉發(fā)實現(xiàn)了參數(shù)在傳遞過程中保持其值屬性的功能,即若是左值,則傳遞之后仍然是左值,若是右值,則傳遞之后仍然是右值。
簡單來說,std::move用于將左值或右值對象強轉成右值語義,而std::forward用于保持左值對象的左值語義和右值對象的右值語義。
4.2 Why?
#include <utility>
#include <iostream>
void bar(const int& x)
{
std::cout << "lvalue" << std::endl;
}
void bar(int&& x)
{
std::cout << "rvalue" << std::endl;
}
template <typename T>
void foo(T&& x)
{
bar(x);
}
int main()
{
int x = 10;
foo(x); // 輸出:lvalue
foo(10); // 輸出:lvalue
return 0;
}
執(zhí)行以上代碼會發(fā)現(xiàn),foo(x)和foo(10)都會輸出lvalue。foo(x)輸出lvalue可以理解,因為x是左值嘛,但是10是右值,為啥foo(10)也輸出lvalue呢?
這是因為10只是作為函數(shù)foo的右值參數(shù),但是在foo內部,10被帶入了形參x,而x是一個有名字的變量,即右值,因此foo中bar(x)還是輸出lvalue。
那么問題來了,如果我們想在foo函數(shù)內部保持x的右值語義,該怎么做呢?std::forward便派上了用場。
只需改寫foo函數(shù):
template <typename T>
void foo(T&& x)
{
bar(std::forward<T>(x));
}
4.3 How?
std::forward聽起來有點神奇,那么它到底是如何實現(xiàn)的呢?
template<typename T, typename Arg>
shared_ptr<T> factory(Arg&& arg)
{
return shared_ptr<T>(new T(std::forward<Arg>(arg)));
}
template<class S>
S&& forward(typename remove_reference<S>::type& a) noexcept
{
return static_cast<S&&>(a);
}
X x;
factory<A>(x);
如果factory的輸入?yún)?shù)是一個左值,那么Arg = X&,根據(jù)疊加規(guī)則,std::forward<Arg> = X&。因此,在這種情況下,std::forward<Arg>(arg)仍然是左值。
相反,如果factory輸入?yún)?shù)是一個右值,那么Arg = X,std::forward<Arg> = X。這種情況下,std::forward<Arg>(arg)是一個右值。
恰好達到了保留左值or右值語義的效果!
4.4 Example
直接上代碼。如果前面都懂了,相信這段代碼的輸出結果也能猜個八九不離十了。
#include <utility>
#include <iostream>
void overloaded(const int& x)
{
std::cout << "[lvalue]" << std::endl;
}
void overloaded(int&& x)
{
std::cout << "[rvalue]" << std::endl;
}
template <class T> void fn(T&& x)
{
overloaded(x);
overloaded(std::forward<T>(x));
}
int main()
{
int i = 10;
overloaded(std::forward<int>(i));
overloaded(std::forward<int&>(i));
overloaded(std::forward<int&&>(i));
fn(i);
fn(std::move(i));
return 0;
}
以上就是C++ 右值語義相關總結的詳細內容,更多關于C++ 右值語義的資料請關注腳本之家其它相關文章!
相關文章
C++利用數(shù)組(一維/二維)處理批量數(shù)據(jù)的方法
對于簡單的問題,使用簡單的數(shù)據(jù)類型就可以了,但是對于有些需要處理的數(shù)據(jù),只用以上簡單的數(shù)據(jù)類型是不夠的,難以反映出數(shù)據(jù)的特點,也難以有效的進行處理,本文小編給大家介紹了C++利用數(shù)組(一維/二維)處理批量數(shù)據(jù)的方法,需要的朋友可以參考下2023-10-10

