C++在同一對象中存儲左值或右值的方法
一、背景
C++ 代碼似乎經(jīng)常出現(xiàn)一個問題:如果該值可以來自左值或右值,則對象如何跟蹤該值?即如果保留該值作為引用,那么就無法綁定到臨時對象。如果將其保留為一個值,那么當它從左值初始化時,會產(chǎn)生不必要的副本。
有幾種方法可以應對這種情況。使用std::variant提供了一個很好的折衷方案來獲得有表現(xiàn)力的代碼。
二、跟蹤值
假設有一個類MyClass
。想讓MyClass
訪問某個std::string
。如何表示MyClass
內(nèi)部的字符串?
有兩種選擇:
- 將其存儲為引用。
- 將其存儲為副本。
2.1、存儲引用
如果將其存儲為引用,例如const引用:
class MyClass { public: explicit MyClass(std::string const& s) : s_(s) {} void print() const { std::cout << s_ << '\n'; } private: std::string const& s_; };
則可以用一個左值初始化我們的引用:
std::string s = "hello"; MyClass myObject{s}; myObject.print();
看起來很不錯。但是,如果想用右值初始化我們的對象呢?例如:
MyClass myObject{std::string{"hello"}}; myObject.print();
或者這樣的代碼:
std::string getString(); // function declaration returning by value MyClass myObject{getString()}; myObject.print();
那么代碼具有未定義的行為。原因是,臨時字符串對象在創(chuàng)建它的同一條語句中被銷毀。當調用print
時,字符串已經(jīng)被破壞,使用它是非法的,并導致未定義的行為。
為了說明這一點,如果將std::string
替換為類型X
,并且在X
的析構函數(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_; };
在調用的地方也打印日志:
MyClass myObject(X{}); std::cout << "before print" << '\n'; myObject.print();
輸出:
X destroyed before print
可以看到,在嘗試使用之前,這個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、存儲值
另一種選擇是存儲一個值。這允許使用move
語義將傳入的臨時值移動到存儲值中:
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)在調用它:
MyClass myObject{std::string{"hello"}}; myObject.print();
產(chǎn)生兩次移動(一次構造s
,一次構造s_
),并且沒有未定義的行為。實際上,即使臨時對象被銷毀,print
也會使用類內(nèi)部的實例。
不幸的是,如果帶著左值返回到第一個調用點:
std::string s = "hello"; MyClass myObject{s}; myObject.print();
那么就不再做兩次移動了:做了一次復制(構造s)和一次移動(構造s_)。
更重要的是,我們的目的是給MyClass訪問字符串的權限,如果做一個拷貝,就有了一個不同于進來的實例。所以它們不會同步。
對于臨時對象來說,這不是問題,因為它無論如何都會被銷毀,并且我們在之前將它移了進來,所以仍然可以訪問字符串。但是通過復制,我們不再給MyClass訪問傳入字符串的權限。
所以存儲一個值也不是一個好的解決方案。
三、存儲variant
存儲引用不是一個好的解決方案,存儲值也不是一個好的解決方案。我們想做的是,如果引用是從左值初始化的,則存儲引用;如果引用是從右值初始化的,則存儲引用。
但是數(shù)據(jù)成員只能是一種類型:值或引用,對嗎?
但是,對于std::variant
,它可以是任意一個。不過,如果嘗試在一個變量中存儲引用,就像這樣:
std::variant<std::string, std::string const&>
將得到一個編譯錯誤:
variant must have no reference alternative
為了達到我們的目的,需要將引用放在另一個類型中;即必須編寫特定的代碼來處理數(shù)據(jù)成員。如果為std::string
編寫這樣的代碼,則不能將其用于其他類型。
在這一點上,最好以通用的方式編寫代碼。
四、通用存儲類
存儲需要是一個值或一個引用。既然現(xiàn)在是為通用目的編寫這段代碼,那么也可以允許非const
引用。由于變量不能直接保存引用,那么可以將它們存儲到包裝器中:
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)) {} };
將存儲定義為這兩種情況之一:
template<typename T> using Storage = std::variant<Value<T>, ConstReference<T>, NonConstReference<T>>;
現(xiàn)在需要通過提供引用來訪問變量的底層值。創(chuàng)建了兩種類型的訪問:一種是const
,另一種是非const
。
4.1、定義const訪問
要定義const
訪問,需要使變量內(nèi)部的三種可能類型中的每一種都產(chǎn)生一個const
引用。
為了訪問變量中的數(shù)據(jù),將使用std::visit
和規(guī)范的overload
模式,這可以在c++ 17中實現(xiàn):
template<typename... Functions> struct overload : Functions... { using Functions::operator()...; overload(Functions... functions) : Functions(functions)... {} };
要獲得const
引用,只需為每種variant
創(chuàng)建一個:
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)建使用相同的技術,除了variant
是ConstReference
之外,它不能產(chǎn)生非const引用。然而,當std::visit
訪問一個變量時,必須為它的每一個可能的類型編寫代碼:
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 ); }
進一步優(yōu)化,拋出一個異常:
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)建存儲
已經(jīng)定義了存儲類,可以在示例中使用它來訪問傳入的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)調用時帶左值:
std::string s = "hello"; MyClass myObject{s}; myObject.print();
匹配第一個構造函數(shù),并在存儲成員內(nèi)部創(chuàng)建一個NonConstReference
。當print
函數(shù)調用getConstReference
時,非const
引用被轉換為const
引用。
(2)使用臨時值:
MyClass myObject{std::string{"hello"}}; myObject.print();
這個函數(shù)匹配第三個構造函數(shù),并將值移動到存儲中。getConstReference然后將該值的const引用返回給print函數(shù)。
六、總結
variant為c++中跟蹤左值或右值的經(jīng)典問題提供了一種非常適合的解決方案。這種技術的代碼具有表現(xiàn)力,因為std::variant允許表達與我們的意圖非常接近的東西:“根據(jù)上下文,對象可以是引用或值”。
在C++ 17和std::variant之前,解決這個問題很棘手,導致代碼難以正確編寫。隨著語言的發(fā)展,標準庫變得越來越強大,可以用越來越多的表達性代碼來表達我們的意圖。
以上就是C++在同一對象中存儲左值或右值的方法的詳細內(nèi)容,更多關于C++同一對象存儲左值的資料請關注腳本之家其它相關文章!
相關文章
詳解state狀態(tài)模式及在C++設計模式編程中的使用實例
這篇文章主要介紹了state狀態(tài)模式及在C++設計模式編程中的使用實例,在設計模式中策略用來處理算法變化,而狀態(tài)則是透明地處理狀態(tài)變化,需要的朋友可以參考下2016-03-03