跳转至

14 类型安全、std::optional⚓︎

911 个字 66 行代码 预计阅读时间 5 分钟

类型安全:编程语言预防类型错误的效果程度,保证程序的行为正常。

观察下面的代码:

void removeOddsFromEnd(vector<int>& vec)
{
    while (vec.back() % 2 == 1)
    {
        vec.pop_back();
    }
}

它的问题是:当 vec 是一个空向量时,调用该函数就会产生未定义的行为(undefined behavior):要么函数运行会失败,要么会得到垃圾值,要么得到一些意外的真实存在于内存的值。

解决方法:

  • 修改 while 语句为:while(!vec.empty() && vec.back() % 2 == 1),也就是说我们程序员应该预先设置 vec 为非空的条件,避免未定义的行为发生。
  • 修改 vec.back() 函数,使其能够应对各种情况,从而保证确定性的行为

对于后一种方法,假设我们原来的 vec.back() 函数的定义如下:

valueType& vector<valueType>::back()
{
    return *(begin() + size() - 1);
}

如果 vector 为空,那么返回语句的指针将指向未知的一个内存未知,此时对该指针解引用就会造成未定义的行为。其中一种修改方案是在返回语句前先判断 vector 是否为空(本质上同方法一if (empty()) throw std::out_of_range;

还可以有一个小改进:让函数再返回一个调用是否成功的信息,修改结果如下:

std::pair<bool, valueType&> vector<valueType>::back()
{
    if (empty())
        // 默认 valueType 存在构造函数,但调用成本较高
        return {false, valueType()};
    return {true, *(begin() + size() - 1)};
}

std::optional⚓︎

std::optional 是一种类模板,它的值要么是指定类型 T,要么为空(即 nullopt

注:nullopt \(\ne\) nullptr虽然我不清楚 nullptr 是什么

下面这两条语句的意义相同:

// std::optional<int> num1;
num1 = {};
num1 = std::nullopt;

利用 std::optional,之前的 vec.back() 函数可以修改为:

std::optional<valueType> vector<valueType>::back()
{
    if (empty())
        return {};
    return *(begin() + size() - 1);
}

但这样修改后又出现一个问题:因为 std::optional 类型不能进行算术运算,所以与之前的 removeOddsFromEnd() 函数发生冲突。但是我们可以使用 value() 方法获取它的值,使其可以进行算术运算:

void removeOddsFromEnd(vector<int>& vec)
{
    while(vec.back().has_value() && vec.back().value() % 2 == 1)
    {
        vec.pop_back();
    }
}

也许你认为 while 语句的判断条件有点太长了,想要偷懒一点,将 vec.back().has_value 改为 vec.back()。虽然这样可行,但是请不要这么做具体原因目前不是很清楚

关于 std::optional 的一些接口或方法

  • .value():返回 std::optional 变量的值,或者抛出 bad_optional_access 错误
  • .value_or(valueType val):返回变量的值,或者设置好的默认值 val
  • .has_value():当变量存在值的时候返回 true,否则返回 false
  • .and_then(function f):如果值 value 存在,返回调用 f(value) 的结果,否则为 nulloptf 的返回类型必须是 optional
  • .transform(function f):如果值 value 存在,返回调用 f(value) 的结果,否则为 nulloptf 的返回类型必须是 optional<valueType>
  • or_else(function f):若存在值 value 则返回其本身,否则返回调用 f 的结果
remobeOddsFromEnd() 函数最终版本
void removeOddsFromEnd(vector<int>& vec)
{
    // lambda expression
    auto isOdd = [](optional<int> num)
    {
        if (num)
            return num % 2 == 1;
        else
            return std::nullopt;
    }
    while (vec.back().and_then(isOdd))
    {
        vec.pop_back();
    }
}
例子

下面是一段更烂的代码:

int thisFunctionSucks(vector<int>& vec)
{
    return vec[0];
}

同样的问题:如果 vec 没有任何元素,访问 vec 就是一个未定义的行为。解决方法是不要用方括号访问元素,而是用 .at() 方法访问,相比前一种方法更加安全,这可以从它们的实现代码中看出:

// [] operator
valueType& vector<valueType>::operator[](size_t index)
{
    return *(begin() + index);
}

// .at() method
valueType& vector<valueType>::at(size_t index)
{
    if (index >= size()) 
        throw std::out_of_range; // 它会报错!
    return *(begin() + index);
}

使用 std::optional 作为函数返回类型的优劣:

  • 优:
    • 能够使函数返回更有意义的内容
    • 类函数调用行为的正确性、安全性得到保证
  • 劣:
    • 需要到处使用 .value() 方法
    • (在 C++ 中)很可能会遇到 bad_optional_access 错误
    • (在 C++ 中)该类型自身也存在未定义的行为(比如在使用 .value() 时没有进行错误检查)
    • ...

由于 std::optional 比较笨重,且运行速度慢,所以 C++ STL 的数据结构一般不会用到它。但是很多别的编程语言会用到类似 std::optional 的东西,比如 Rust、Swift、JavaScript 等。

评论区

如果大家有什么问题或想法,欢迎在下方留言~