【问题标题】:How to protect user if can't return nullptr for an empty stack?如果无法为空堆栈返回 nullptr,如何保护用户?
【发布时间】:2020-03-08 14:55:25
【问题描述】:

我正在尝试编写 Coursera course example of Towers of Hanoi 中给出的 Cube、Stack 示例的实现,以了解更多 C++。

在stack.h我必须实现:

class Stack {
public:
void push_back(const Cube & cube);
Cube removeTop();
Cube & peekTop();
unsigned size() const;

friend std::ostream & operator<<(std::ostream & os, const Stack & stack);

private:
std::vector<Cube> cubes_;
};

我遇到的问题是removeTop()。如果堆栈(向量)为空,我正在考虑返回nullptr,因为pop_back 的行为对于空向量是未定义的。

在空容器上调用 pop_back 是未定义的。 Cpp Reference

inline Cube Stack::removeTop() {
  if (!cubes_.empty()) {
    Cube top_cube = cubes_.back();
    cubes_.pop_back();
    return top_cube;
  }
  else {
    return nullptr;
  }
}

但是,我在编译过程中遇到错误。

./stack.h:35:12: error: no viable conversion from returned value of type
      'std::__1::nullptr_t' to function return type 'uiuc::Cube'
    return nullptr;

如果我无法返回nullptr,我该如何保护用户?我是否仅限于告诉用户不应在空堆栈上调用该函数并让他/她负责检查?

【问题讨论】:

  • 这是一种选择。另一种选择是使用std::optional 作为返回值。如果您不熟悉如何使用std::optional,您应该在您的 C++ 书籍中找到更多文档。
  • @SamVarshavchik,谢谢。这不是一本书,而是一个视频课程。我会调查std::optional。还没到。在查看建议之前,我是自己编码的。
  • 如果Cube 是指针,或者具有接受指针的构造函数,则返回Cube 的函数只能返回nullptr。您的选项包括 (1) 传递对 Cube 的引用并返回 int 或 bool,用于指示是否可以从堆栈中弹出 (2) 返回 std::optional (3) 引发异常如果堆栈为空。选项 (1) 和 (2) 要求调用者进行检查。选项(3)强制调用者(或调用者的调用者)捕获异常(否则程序将终止)。
  • @Peter:你为什么不发表你的评论作为答案?

标签: c++ stack nullptr


【解决方案1】:

这正是例外情况:

if (cubes_.empty())
    throw std::runtime_error("stack underflow");
Cube top_cube = cubes_.back();
cubes_.pop_back();
return top_cube;

将这与std::optional 复杂化几乎肯定不是正确的答案。试图从空堆栈中弹出意味着程序迷路了。这应该是一个硬错误,而不是被显示“您可能有也可能没有,请稍后检查”的界面所掩盖的错误。

【讨论】:

  • 完全正确。在空堆栈上调用 pop 绝对是一个硬错误。
【解决方案2】:

可能是这样的:

inline bool Stack::removeTop(Cube& top_cube) {
  if (!cubes_.empty()) {
    top_cube = cubes_.back();
    cubes_.pop_back();
    return true;
  }
  else {
    return false;
  }
}

【讨论】:

  • 这种方法不受欢迎的原因有很多:它总是构造一个对象,然后复制分配它。因此,当复制分配和/或构造是一项昂贵的操作时,这将不是一个理想的方法。如果对象有一个已删除的复制赋值运算符,它根本就不起作用。
【解决方案3】:

基于函数签名,你必须实现,你真的不能。通常,你会用断言来保护这种事情。在某些情况下,您可能会使用NullObject pattern,或者您可能会返回一个垃圾对象。在较新的 C++ 版本中,您还可以使用 std::optional&lt;T&gt;。

inline Cube Stack::removeTop() {
  if (!cubes_.empty()) {
    Cube top_cube = cubes_.back();
    cubes_.pop_back();
    return top_cube;
  }
  else {
    return Cube {};
  }
}

【讨论】:

    【解决方案4】:

    正如pyj 所指出的,从 c++17 开始,有一种新机制可以做到这一点(采用 boost::optional 思想)。这是 std::optional 容器。当使用std::optional时,你告诉这个函数的用户,他可能会得到一个empty response,因此他必须检查它是否被初始化。

    来自 cpp 参考:

    类模板 std::optional 管理一个可选的包含值,即一个可能存在也可能不存在的值。

    可选的常见用例是可能失败的函数的返回值。与 std::pair 等其他方法相比, optional 可以很好地处理构造成本高的对象并且更具可读性,因为其意图是明确表达的。

    现在使用:

    inline std::optional<Cube> Stack::removeTop() 
    {
        if (!cubes_.empty()) 
        {
            Cube top_cube = cubes_.back();
            cubes_.pop_back();
            return top_cube;
        }
        else 
        {
            return std::nullopt;
        }
    }
    

    而且,虽然您获得的是 std::optional,而不是 Cube,但堆上没有 内存分配,这比使用指针获得相同的输出有很大的优势。

    如果一个可选项包含一个值,则该值保证作为可选对象占用空间的一部分进行分配,即不会发生动态内存分配。因此,可选对象建模对象,而不是指针,即使定义了 operator*() 和 operator->()。

    【讨论】:

    • 为什么不直接返回tob_cube 而是使用大括号?
    • 如果我没记错的话,它们不是必需的(从示例中删除它们)。但是,您应该注意到返回类型 不是 Cube - 它是 std::optional,它具有返回 Cube 的 operator*。
    • operator* 对空的可选项的结果是什么?
    • 根据 cpp 参考结果未定义(“如果 *this 不包含值,则行为未定义。”)。 然而,这是一个习惯用法——如果你使用std::optional,你必须检查它是否被初始化。这就像使用指针而不检查它是否为 nullptr。
    • 我可以看到的 macOS 错误与此功能的实验性版本有关,我会尝试在更新版本的编译器中使用 #include (而不是实验性/可选!)。他们现在都应该正确支持它。
    猜你喜欢
    • 1970-01-01
    • 2021-02-16
    • 1970-01-01
    • 2023-04-03
    • 2016-02-12
    • 2011-07-30
    • 1970-01-01
    • 2019-10-06
    • 2017-08-28
    相关资源
    最近更新 更多