C++在同一對(duì)象中存儲(chǔ)左值或右值的方法
一、背景
C++ 代碼似乎經(jīng)常出現(xiàn)一個(gè)問題:如果該值可以來自左值或右值,則對(duì)象如何跟蹤該值?即如果保留該值作為引用,那么就無(wú)法綁定到臨時(shí)對(duì)象。如果將其保留為一個(gè)值,那么當(dāng)它從左值初始化時(shí),會(huì)產(chǎn)生不必要的副本。
有幾種方法可以應(yīng)對(duì)這種情況。使用std::variant提供了一個(gè)很好的折衷方案來獲得有表現(xiàn)力的代碼。
二、跟蹤值
假設(shè)有一個(gè)類MyClass
。想讓MyClass
訪問某個(gè)std::string
。如何表示MyClass
內(nèi)部的字符串?
有兩種選擇:
- 將其存儲(chǔ)為引用。
- 將其存儲(chǔ)為副本。
2.1、存儲(chǔ)引用
如果將其存儲(chǔ)為引用,例如const引用:
class MyClass { public: explicit MyClass(std::string const& s) : s_(s) {} void print() const { std::cout << s_ << '\n'; } private: std::string const& s_; };
則可以用一個(gè)左值初始化我們的引用:
std::string s = "hello"; MyClass myObject{s}; myObject.print();
看起來很不錯(cuò)。但是,如果想用右值初始化我們的對(duì)象呢?例如:
MyClass myObject{std::string{"hello"}}; myObject.print();
或者這樣的代碼:
std::string getString(); // function declaration returning by value MyClass myObject{getString()}; myObject.print();
那么代碼具有未定義的行為。原因是,臨時(shí)字符串對(duì)象在創(chuàng)建它的同一條語(yǔ)句中被銷毀。當(dāng)調(diào)用print
時(shí),字符串已經(jīng)被破壞,使用它是非法的,并導(dǎo)致未定義的行為。
為了說明這一點(diǎn),如果將std::string
替換為類型X
,并且在X
的析構(gòu)函數(shù)打印日志:
struct X { ~X() { std::cout << "X destroyed" << '\n';} }; class MyClass { public: explicit MyClass(X const& x) : x_(x) {} void print() const { // using x_; } private: X const& x_; };
在調(diào)用的地方也打印日志:
MyClass myObject(X{}); std::cout << "before print" << '\n'; myObject.print();
輸出:
X destroyed before print
可以看到,在嘗試使用之前,這個(gè)X
已經(jīng)被破壞了。
完整示例:
#include <iostream> #include <string> struct X { ~X() { std::cout << "X destroyed" << '\n';} }; class MyClass { public: explicit MyClass(X const& x) : x_(x) {} void print() { (void) x_; // using x_; } private: X const& x_; }; int main() { MyClass myObject(X{}); std::cout << "before print" << '\n'; myObject.print(); }
2.2、存儲(chǔ)值
另一種選擇是存儲(chǔ)一個(gè)值。這允許使用move
語(yǔ)義將傳入的臨時(shí)值移動(dòng)到存儲(chǔ)值中:
class MyClass { public: explicit MyClass(std::string s) : s_(std::move(s)) {} void print() const { std::cout << s_ << '\n'; } private: std::string s_; };
現(xiàn)在調(diào)用它:
MyClass myObject{std::string{"hello"}}; myObject.print();
產(chǎn)生兩次移動(dòng)(一次構(gòu)造s
,一次構(gòu)造s_
),并且沒有未定義的行為。實(shí)際上,即使臨時(shí)對(duì)象被銷毀,print
也會(huì)使用類內(nèi)部的實(shí)例。
不幸的是,如果帶著左值返回到第一個(gè)調(diào)用點(diǎn):
std::string s = "hello"; MyClass myObject{s}; myObject.print();
那么就不再做兩次移動(dòng)了:做了一次復(fù)制(構(gòu)造s)和一次移動(dòng)(構(gòu)造s_)。
更重要的是,我們的目的是給MyClass訪問字符串的權(quán)限,如果做一個(gè)拷貝,就有了一個(gè)不同于進(jìn)來的實(shí)例。所以它們不會(huì)同步。
對(duì)于臨時(shí)對(duì)象來說,這不是問題,因?yàn)樗鼰o(wú)論如何都會(huì)被銷毀,并且我們?cè)谥皩⑺屏诉M(jìn)來,所以仍然可以訪問字符串。但是通過復(fù)制,我們不再給MyClass訪問傳入字符串的權(quán)限。
所以存儲(chǔ)一個(gè)值也不是一個(gè)好的解決方案。
三、存儲(chǔ)variant
存儲(chǔ)引用不是一個(gè)好的解決方案,存儲(chǔ)值也不是一個(gè)好的解決方案。我們想做的是,如果引用是從左值初始化的,則存儲(chǔ)引用;如果引用是從右值初始化的,則存儲(chǔ)引用。
但是數(shù)據(jù)成員只能是一種類型:值或引用,對(duì)嗎?
但是,對(duì)于std::variant
,它可以是任意一個(gè)。不過,如果嘗試在一個(gè)變量中存儲(chǔ)引用,就像這樣:
std::variant<std::string, std::string const&>
將得到一個(gè)編譯錯(cuò)誤:
variant must have no reference alternative
為了達(dá)到我們的目的,需要將引用放在另一個(gè)類型中;即必須編寫特定的代碼來處理數(shù)據(jù)成員。如果為std::string
編寫這樣的代碼,則不能將其用于其他類型。
在這一點(diǎn)上,最好以通用的方式編寫代碼。
四、通用存儲(chǔ)類
存儲(chǔ)需要是一個(gè)值或一個(gè)引用。既然現(xiàn)在是為通用目的編寫這段代碼,那么也可以允許非const
引用。由于變量不能直接保存引用,那么可以將它們存儲(chǔ)到包裝器中:
template<typename T> struct NonConstReference { T& value_; explicit NonConstReference(T& value) : value_(value){}; }; template<typename T> struct ConstReference { T const& value_; explicit ConstReference(T const& value) : value_(value){}; }; template<typename T> struct Value { T value_; explicit Value(T&& value) : value_(std::move(value)) {} };
將存儲(chǔ)定義為這兩種情況之一:
template<typename T> using Storage = std::variant<Value<T>, ConstReference<T>, NonConstReference<T>>;
現(xiàn)在需要通過提供引用來訪問變量的底層值。創(chuàng)建了兩種類型的訪問:一種是const
,另一種是非const
。
4.1、定義const訪問
要定義const
訪問,需要使變量?jī)?nèi)部的三種可能類型中的每一種都產(chǎn)生一個(gè)const
引用。
為了訪問變量中的數(shù)據(jù),將使用std::visit
和規(guī)范的overload
模式,這可以在c++ 17中實(shí)現(xiàn):
template<typename... Functions> struct overload : Functions... { using Functions::operator()...; overload(Functions... functions) : Functions(functions)... {} };
要獲得const
引用,只需為每種variant
創(chuàng)建一個(gè):
template<typename T> T const& getConstReference(Storage<T> const& storage) { return std::visit( overload( [](Value<T> const& value) -> T const& { return value.value_; }, [](NonConstReference<T> const& value) -> T const& { return value.value_; }, [](ConstReference<T> const& value) -> T const& { return value.value_; } ), storage ); }
4.2、定義非const訪問
非const引用的創(chuàng)建使用相同的技術(shù),除了variant
是ConstReference
之外,它不能產(chǎn)生非const引用。然而,當(dāng)std::visit
訪問一個(gè)變量時(shí),必須為它的每一個(gè)可能的類型編寫代碼:
template<typename T> T& getReference(Storage<T>& storage) { return std::visit( overload( [](Value<T>& value) -> T& { return value.value_; }, [](NonConstReference<T>& value) -> T& { return value.value_; }, [](ConstReference<T>& ) -> T&. { /* code handling the error! */ } ), storage ); }
進(jìn)一步優(yōu)化,拋出一個(gè)異常:
struct NonConstReferenceFromReference : public std::runtime_error { explicit NonConstReferenceFromReference(std::string const& what) : std::runtime_error{what} {} }; template<typename T> T& getReference(Storage<T>& storage) { return std::visit( overload( [](Value<T>& value) -> T& { return value.value_; }, [](NonConstReference<T>& value) -> T& { return value.value_; }, [](ConstReference<T>& ) -> T& { throw NonConstReferenceFromReference{"Cannot get a non const reference from a const reference"} ; } ), storage ); }
五、創(chuàng)建存儲(chǔ)
已經(jīng)定義了存儲(chǔ)類,可以在示例中使用它來訪問傳入的std::string
,而不管它的值類別:
class MyClass { public: explicit MyClass(std::string& value) : storage_(NonConstReference(value)){} explicit MyClass(std::string const& value) : storage_(ConstReference(value)){} explicit MyClass(std::string&& value) : storage_(Value(std::move(value))){} void print() const { std::cout << getConstReference(storage_) << '\n'; } private: Storage<std::string> storage_; };
(1)調(diào)用時(shí)帶左值:
std::string s = "hello"; MyClass myObject{s}; myObject.print();
匹配第一個(gè)構(gòu)造函數(shù),并在存儲(chǔ)成員內(nèi)部創(chuàng)建一個(gè)NonConstReference
。當(dāng)print
函數(shù)調(diào)用getConstReference
時(shí),非const
引用被轉(zhuǎn)換為const
引用。
(2)使用臨時(shí)值:
MyClass myObject{std::string{"hello"}}; myObject.print();
這個(gè)函數(shù)匹配第三個(gè)構(gòu)造函數(shù),并將值移動(dòng)到存儲(chǔ)中。getConstReference然后將該值的const引用返回給print函數(shù)。
六、總結(jié)
variant為c++中跟蹤左值或右值的經(jīng)典問題提供了一種非常適合的解決方案。這種技術(shù)的代碼具有表現(xiàn)力,因?yàn)閟td::variant允許表達(dá)與我們的意圖非常接近的東西:“根據(jù)上下文,對(duì)象可以是引用或值”。
在C++ 17和std::variant之前,解決這個(gè)問題很棘手,導(dǎo)致代碼難以正確編寫。隨著語(yǔ)言的發(fā)展,標(biāo)準(zhǔn)庫(kù)變得越來越強(qiáng)大,可以用越來越多的表達(dá)性代碼來表達(dá)我們的意圖。
以上就是C++在同一對(duì)象中存儲(chǔ)左值或右值的方法的詳細(xì)內(nèi)容,更多關(guān)于C++同一對(duì)象存儲(chǔ)左值的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C++結(jié)構(gòu)體初始化的10種寫法總結(jié)
這篇文章主要為大家詳細(xì)介紹了10種C++中結(jié)構(gòu)體初始化的寫法,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-04-04linux下基于C語(yǔ)言的信號(hào)編程實(shí)例
這篇文章主要介紹了linux下基于C語(yǔ)言的信號(hào)編程,實(shí)例分析了信號(hào)量的基本使用技巧與相關(guān)概念,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-07-07詳解state狀態(tài)模式及在C++設(shè)計(jì)模式編程中的使用實(shí)例
這篇文章主要介紹了state狀態(tài)模式及在C++設(shè)計(jì)模式編程中的使用實(shí)例,在設(shè)計(jì)模式中策略用來處理算法變化,而狀態(tài)則是透明地處理狀態(tài)變化,需要的朋友可以參考下2016-03-03使用OpenCV實(shí)現(xiàn)檢測(cè)和追蹤車輛
這篇文章主要為大家詳細(xì)介紹了使用OpenCV實(shí)現(xiàn)檢測(cè)和追蹤車輛,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01C++繼承中的對(duì)象構(gòu)造與析構(gòu)和賦值重載詳解
這篇文章主要為大家詳細(xì)介紹了C++繼承中的對(duì)象構(gòu)造與析構(gòu)和賦值重載,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-03-03