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)建它的同一條語句中被銷毀。當調(diào)用print時,字符串已經(jīng)被破壞,使用它是非法的,并導致未定義的行為。
為了說明這一點,如果將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
可以看到,在嘗試使用之前,這個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)在調(diào)用它:
MyClass myObject{std::string{"hello"}};
myObject.print();
產(chǎn)生兩次移動(一次構(gòu)造s,一次構(gòu)造s_),并且沒有未定義的行為。實際上,即使臨時對象被銷毀,print也會使用類內(nèi)部的實例。
不幸的是,如果帶著左值返回到第一個調(diào)用點:
std::string s = "hello";
MyClass myObject{s};
myObject.print();
那么就不再做兩次移動了:做了一次復制(構(gòu)造s)和一次移動(構(gòu)造s_)。
更重要的是,我們的目的是給MyClass訪問字符串的權(quán)限,如果做一個拷貝,就有了一個不同于進來的實例。所以它們不會同步。
對于臨時對象來說,這不是問題,因為它無論如何都會被銷毀,并且我們在之前將它移了進來,所以仍然可以訪問字符串。但是通過復制,我們不再給MyClass訪問傳入字符串的權(quán)限。
所以存儲一個值也不是一個好的解決方案。
三、存儲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)建使用相同的技術(shù),除了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)調(diào)用時帶左值:
std::string s = "hello";
MyClass myObject{s};
myObject.print();
匹配第一個構(gòu)造函數(shù),并在存儲成員內(nèi)部創(chuàng)建一個NonConstReference。當print函數(shù)調(diào)用getConstReference時,非const引用被轉(zhuǎn)換為const引用。
(2)使用臨時值:
MyClass myObject{std::string{"hello"}};
myObject.print();
這個函數(shù)匹配第三個構(gòu)造函數(shù),并將值移動到存儲中。getConstReference然后將該值的const引用返回給print函數(shù)。
六、總結(jié)
variant為c++中跟蹤左值或右值的經(jīng)典問題提供了一種非常適合的解決方案。這種技術(shù)的代碼具有表現(xiàn)力,因為std::variant允許表達與我們的意圖非常接近的東西:“根據(jù)上下文,對象可以是引用或值”。
在C++ 17和std::variant之前,解決這個問題很棘手,導致代碼難以正確編寫。隨著語言的發(fā)展,標準庫變得越來越強大,可以用越來越多的表達性代碼來表達我們的意圖。
以上就是C++在同一對象中存儲左值或右值的方法的詳細內(nèi)容,更多關(guān)于C++同一對象存儲左值的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C++結(jié)構(gòu)體初始化的10種寫法總結(jié)
這篇文章主要為大家詳細介紹了10種C++中結(jié)構(gòu)體初始化的寫法,文中的示例代碼講解詳細,具有一定的借鑒價值,感興趣的小伙伴可以跟隨小編一起學習一下2024-04-04
詳解state狀態(tài)模式及在C++設計模式編程中的使用實例
這篇文章主要介紹了state狀態(tài)模式及在C++設計模式編程中的使用實例,在設計模式中策略用來處理算法變化,而狀態(tài)則是透明地處理狀態(tài)變化,需要的朋友可以參考下2016-03-03
C++繼承中的對象構(gòu)造與析構(gòu)和賦值重載詳解
這篇文章主要為大家詳細介紹了C++繼承中的對象構(gòu)造與析構(gòu)和賦值重載,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-03-03

