17

std::vector将成员函数at()作为 的安全替代方法operator[],因此应用绑定检查并且不创建悬空引用:

void foo(std::vector<int> const&x)
{
  const auto&a=x[0];     // What if x.empty()? Undefined behavior!
  const auto&a=x.at(0);  // Throws exception if x.empty().
}

但是,std::unique_ptr缺少相应的功能:

void foo(std::unique_ptr<int> const&x)
{
  const auto&a=*x;       // What if bool(x)==false? Undefined behavior!
}

如果有这样一个安全的替代方案,那就太好std::unique_ptr了,比如成员ref()(and cref()),它从不返回悬空引用,而是抛出异常。可能的实现:

template<typename T>
typename add_lvalue_reference<T>::type
unique_ptr<T>::ref() const noexcept(false)
{
  if(bool(*this)==false)
    throw run_time_error("trying to de-refrence null unique_ptr");
  return this->operator*();
}

标准不提供这种东西有什么好的理由吗?

4

6 回答 6

17

unique_ptr专门设计为具有空状态检测的轻量级指针类(例如,在A 提案中的optional中声明,添加一个实用程序类来表示可选对象(修订版 3) )

也就是说,您所要求的功能已经到位,因为operator*文档指出:

// may throw, e.g. if pointer defines a throwing operator*
typename std::add_lvalue_reference<T>::type operator*() const;

pointer类型定义

std::remove_reference<Deleter>::type::pointer if that type exists, otherwise T*

因此,通过您的自定义删除器,您可以执行任何即时操作,包括空指针检查和异常抛出

#include <iostream>
#include <memory>

struct Foo { // object to manage
    Foo() { std::cout << "Foo ctor\n"; }
    Foo(const Foo&) { std::cout << "Foo copy ctor\n"; }
    Foo(Foo&&) { std::cout << "Foo move ctor\n"; }
    ~Foo() { std::cout << "~Foo dtor\n"; }
};

struct Exception {};

struct InternalPtr {
    Foo *ptr = nullptr;
    InternalPtr(Foo *p) : ptr(p) {}
    InternalPtr() = default;

    Foo& operator*() const {
        std::cout << "Checking for a null pointer.." << std::endl;
        if(ptr == nullptr)
            throw Exception();
        return *ptr;
    }

    bool operator != (Foo *p) {
        if(p != ptr)
            return false;
        else
            return true;
    }
    void cleanup() {
      if(ptr != nullptr)
        delete ptr;
    }
};

struct D { // deleter
    using pointer = InternalPtr;
    D() {};
    D(const D&) { std::cout << "D copy ctor\n"; }
    D(D&) { std::cout << "D non-const copy ctor\n";}
    D(D&&) { std::cout << "D move ctor \n"; }
    void operator()(InternalPtr& p) const {
        std::cout << "D is deleting a Foo\n";
        p.cleanup();
    };
};

int main()
{
    std::unique_ptr<Foo, D> up(nullptr, D()); // deleter is moved

    try {
      auto& e = *up;      
    } catch(Exception&) {
        std::cout << "null pointer exception detected" << std::endl;
    }

}

Live Example

为了完整起见,我将发布两个额外的替代方案/解决方法:

  1. 指针检查unique_ptr通孔operator bool

    #include <iostream>
    #include <memory>
    
    int main()
    {
        std::unique_ptr<int> ptr(new int(42));
    
        if (ptr) std::cout << "before reset, ptr is: " << *ptr << '\n';
        ptr.reset();
        if (ptr) std::cout << "after reset, ptr is: " << *ptr << '\n';
    }
    

    (这可能是处理这个问题的最隐秘的方式)

  2. 另一种解决方案,虽然比较麻烦,是使用负责异常处理的包装器类型

于 2015-08-18T08:40:08.490 回答
13

我怀疑真正的答案很简单,对于很多“为什么 C++ 不像这样?”也是一样的。问题:

没有人提议。

std::vector并且std::unique_ptr不是由同一个人同时设计的,也不是以相同的方式使用的,所以不一定遵循相同的设计原则。

于 2015-08-18T09:03:11.627 回答
4

我不能说,为什么委员会决定不添加安全的取消引用方法 - 答案可能是“因为它没有被提议”“因为原始指针也没有”。但是自己编写一个自由函数模板是微不足道的,它将任何指针作为参数,将其与 nullptr 进行比较,然后抛出异常或返回对指向对象的引用。

如果您不通过指向基类的指针删除它,甚至应该可以从 a 公开派生unique_ptr并添加这样的成员函数。

但是请记住,在任何地方使用这种检查方法可能会导致显着的性能损失(与 at 相同)。通常,您最多希望验证一次参数,而开头的单个 if 语句更适合。

还有一所学校说你不应该抛出异常来响应编程错误。也许负责设计unique_ptr的人属于这所学校,而设计矢量的人(年龄要大得多)则不属于。

于 2015-08-18T08:54:05.767 回答
3

智能指针 API 设计的主要目标之一是成为具有附加值、没有陷阱或副作用以及接近零开销的直接替代品。if (ptr) ptr->...是通常如何安全访问裸指针,相同的语法适用于智能指针,因此当一个替换为另一个时不需要更改代码。

对指针内的有效性进行额外检查(例如,抛出异常)会干扰分支预测器,因此可能会对性能产生连锁反应,这可能不再被视为零成本的直接替代。

于 2015-08-18T08:45:12.130 回答
2

你有

operator bool()

示例来自: cplusplusreference

// example of unique_ptr::operator bool
#include <iostream>
#include <memory>


int main () {
  std::unique_ptr<int> foo;
  std::unique_ptr<int> bar (new int(12));

  if (foo) std::cout << "foo points to " << *foo << '\n';
  else std::cout << "foo is empty\n";

  if (bar) std::cout << "bar points to " << *bar << '\n';
  else std::cout << "bar is empty\n";

  return 0;
}

unique_ptr 是原始指针的简单包装器,当您可以轻松检查布尔条件时无需抛出异常。

编辑:显然operator* 可以抛出。

异常 1) 可能抛出,例如,如果指针定义了抛出运算符*

也许有人可以解释一下 hot 来定义一个投掷操作员*

于 2015-08-18T08:28:46.623 回答
2

根据 MikeMB 的建议,这里有一个可能实现的用于取消引用指针等的自由函数unique_ptr

template<typename T>
inline T& dereference(T* ptr) noexcept(false)
{
  if(!ptr) throw std::runtime_error("attempt to dereference a nullptr");
  return *ptr;
}

template<typename T>
inline T& dereference(std::unique_ptr<T> const& ptr) noexcept(false)
{
  if(!ptr) throw std::runtime_error("attempt to dereference an empty unique_ptr)");
  return *ptr;
}
于 2015-08-18T10:45:47.120 回答