1. 程式人生 > 其它 >optional 解決空指標_從 std::optional<T&> 談起

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++ 標準庫的實現中,它其實是儲存了引用所對應的指標,所以搞到最後操作的還是指標,只是通過一個封裝來規避危險的操作而已。