optional 解決空指標_從 std::optional<T&> 談起
技術標籤:optional 解決空指標
前段時間小狐狸寫了一點 Rust,最近因為要使用 Qt,回去繼續寫 C++,就很想在 C++ 裡面使用 Rust 的許多常用的型別工具。在 Rust 中 Option<&T> 可以很方便地傳遞一個可能為空的引用,寫得糙的話可以替代普通指標,並且避免空指標造成的錯誤。C++ 17 引入了一個名為 optional 的標準庫容器,就有著與 Rust 的 Option 類似的功能。但由於 C++ 本身的一些侷限,想用它偽造一個指標的替代物其實它遠比想象得更復雜。
不過很多同學會問,既然 C++ 本身是有空指標的,為什麼我們需要強行用 std::optional 來替代指標呢?為什麼不能直接返回引用呢?這裡我們看一個例子:
#include <vector> #include <string> #include <iostream> using namespace std; class BlackBox{ public: void insert(const string& item){ container.push_back(item); } string& get_ref(int i){ return container[i]; } private: vector<string> container; }; int main(){ BlackBox box; box.insert("Hello"); box.insert("world"); // 輸出 Hello cout << box.get_ref(0) << endl; box.get_ref(0) = "Hi"; // 輸出修改後的 Hi cout << box.get_ref(0) << endl; // 崩潰或者 undefined behavior cout << box.get_ref(2) << endl; }
這裡我們用一個名為 BlackBox 的類封裝了一個 vector,並且可以通過 get_ref() 來得到某一項元素的引用。之所以選擇引用而不是指標,是因為引用並不能直接建立,只能指向一個由其他物件管理的記憶體空間,因此可以避免一部分指標的問題(當然 Rust 的引用還能夠進行所有權和生命週期檢查,更為安全,在 C++ 裡面我們就將就一下好了)。返回的引用也能更自然地進行賦值等操作。
然而這裡有一個問題:對於最後我們如果試圖獲取一個不存在的元素的引用,應當返回什麼?根據 vector 的文件,訪問一個不存在的元素是未定義的行為,這顯然是不好的。因此我們希望用 optional 將其包裝,如果這個東西不存在,就返回空,而由呼叫者進行檢查。首先我們嘗試不返回引用,而是返回一個值:
#include <optional>
#include <vector>
#include <string>
#include <iostream>
using namespace std;
class BlackBox{
public:
void insert(const string& item){
container.push_back(item);
}
optional<string> get(int i){
if (i >=0 && i < container.size()) {
return optional<string>(container[i]);
} else {
return nullopt;
}
}
private:
vector<string> container;
};
int main(){
BlackBox box;
box.insert("Hello");
box.insert("world");
// 輸出 Hello
cout << box.get(0).value_or("None") << endl;
// 輸出 None
cout << box.get(2).value_or("None") << endl;
}
到這一步一切都很正常,第一個輸出指令成功輸出了 Hello,而第二個則輸出了 None。現在讓我們嘗試換成返回引用:
class BlackBox{
public:
void insert(const string& item){
container.push_back(item);
}
optional<string&> get(int i){
if (i >=0 && i < container.size()) {
return optional<string>(container[i]);
} else {
return nullopt;
}
}
private:
vector<string> container;
};
僅僅將 optional<string> 換成 optional <string&>,就會產生一大堆編譯錯誤,讓人摸不著頭腦。究其根本原因是 C++ 標準庫容器一般而言只能儲存可複製、可用於賦值的型別,而引用並不滿足這個要求。為此我們需要使用一個名為 reference_wrapper 的東西將引用轉化成一個普通物件進行操作。而要從 reference_wrapper 中獲取引用,則要呼叫它的 get() 方法:
#include <optional>
#include <functional>
#include <vector>
#include <string>
#include <iostream>
using namespace std;
class BlackBox{
public:
void insert(const string& item){
container.push_back(item);
}
optional<reference_wrapper<string>> get(int i){
if (i >=0 && i < container.size()) {
return optional<reference_wrapper<string>>(container[i]);
} else {
return nullopt;
}
}
private:
vector<string> container;
};
int main(){
BlackBox box;
box.insert("Hello");
box.insert("world");
auto ref_opt = box.get(0);
if (ref_opt.has_value()) {
// 從 optional 中獲取 reference_wrapper<string>
auto ref = ref_opt.value();
// ref.get() 的返回值是 string& 型別,因此可以輸出、修改
// 這一步輸出 Hello
cout << ref.get() << endl;
ref.get() = "Hi";
} else {
cout << "None" << endl;
}
ref_opt = box.get(0);
if (ref_opt.has_value()) {
// 這裡輸出 Hi
cout << ref_opt.value().get() << endl;
}
// 這裡 ref_opt 得到的是 nullopt
ref_opt = box.get(2);
cout << (ref_opt.has_value() ? ref_opt.value().get() : "None") << endl;
}
整個程式變複雜了許多,然而通過兩層包裝,我們得到了一個安全的返回引用的方式,除了生命週期檢查,基本和 Rust 的 Option<&mut T> 等價,唯一不同的是有一個 get() 方法的開銷,但這個可以讓編譯器內聯優化抹平,也不是什麼問題。
好奇的同學可能會問,reference_wrapper 究竟是什麼魔法,能把引用包裝成可以操作訪問的東西的?在 GNU C++ 標準庫的實現中,它其實是儲存了引用所對應的指標,所以搞到最後操作的還是指標,只是通過一個封裝來規避危險的操作而已。