optional 解决空指针_从 std::optional<T&> 谈起

本文讨论了C++17引入的std::optional如何作为空指针的替代品,以增强安全性。通过一个BlackBox类的例子,展示了如何使用std::optional包装可能不存在的元素引用,以避免未定义行为。同时,解释了为何不能直接返回引用以及如何利用reference_wrapper解决这个问题,最终实现一个类似于Rust Option<&T>的安全机制。
摘要由CSDN通过智能技术生成

前段时间小狐狸写了一点 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++ 标准库的实现中,它其实是保存了引用所对应的指针,所以搞到最后操作的还是指针,只是通过一个封装来规避危险的操作而已。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值