为什么移动后observer_ptr不归零?

Why isn't observer_ptr zeroed after a move?

本文关键字:归零 ptr 移动 observer 为什么      更新时间:2023-10-16

为什么移动操作后observer_ptr不归零?

在其默认构造中正确地设置为nullptr,这确实是有意义的(并且防止指向垃圾)。

因此,当std::move() 'd时,与std::string, std::vector等一样,应归零。

这将使它在一些有意义的原始指针的上下文中成为一个很好的候选者,以及自动在具有原始指针数据成员的类上生成移动操作,就像本例中一样。


编辑

正如@JonathanWakely在评论中指出的(这与前面提到的问题有关):

如果observer_ptr在移动后为null,则可以使用它来实现零规则适用于具有指针成员的类型。这是一个非常有用的功能。

似乎很多人一开始就忽略了这个想法的重点和实用性。

考虑:

template<typename Mutex>
class unique_lock
{
  Mutex* pm;
public:
  unique_lock() : pm() { }
  unique_lock(Mutex& m) : pm(&m) { }
  ~unique_lock() { if (pm) pm->unlock(); }
  unique_lock(unique_lock&& ul) : pm(ul.pm) { ul.pm = nullptr; }
  unique_lock& operator=(unique_lock&& ul)
  {
    unique_lock(std::move(ul)).swap(*this);
    return *this;
  }
  void swap(unique_lock& ul) { std::swap(pm, ul.pm); }
};

使用默认构造为空且移动后为空的"哑"智能指针,您可以默认三个特殊成员函数,因此它变成:

template<typename Mutex>
class unique_lock
{
  tidy_ptr<Mutex> pm;
public:
  unique_lock() = default;                            // 1
  unique_lock(Mutex& m) : pm(&m) { }
  ~unique_lock() { if (pm) pm->unlock(); }
  unique_lock(unique_lock&& ul) = default;            // 2
  unique_lock& operator=(unique_lock&& ul) = default; // 3
  void swap(unique_lock& ul) { std::swap(pm, ul.pm); }
};

这就是为什么有一个哑的、不拥有的、移动后为空的智能指针很有用,比如tidy_ptr

但是observer_ptr只是默认为空的构造,所以如果它是标准化的,它将有助于声明一个函数接受一个非拥有的指针,但它不会对像上面这样的类有用,所以我仍然需要另一个非拥有的哑指针类型。拥有两个不拥有的愚蠢智能指针类型似乎比没有更糟糕!

因此,move构造函数的设计是为了在某些情况下降低复制构造函数的成本。

让我们写出我们期望这些构造函数和析构函数是什么:(这有点简化,但对于本例来说很好)

observer_ptr() {
    this->ptr == nullptr;
}
observer_ptr(T *obj) {
    this->ptr = obj;
}
observer_ptr(observer_ptr<T> const & obj) {
    this->ptr = obj.ptr;
}
~observer_ptr() {
}

你建议类提供一个移动构造函数,看起来像:

observer_ptr(observer_ptr<T> && obj) {
    this->ptr = obj.ptr;
    obj.ptr = null;
}

当我建议现有的复制构造函数可以正常工作,并且比建议的移动构造函数便宜时。

那么std::vector呢?

std::vector对象在被复制时,实际上是复制它所返回的数组。因此,std::vector复制构造函数看起来像:

vector(vector<T> const & obj) {
    for (auto const & elem : obj)
        this->push_back(elem);
}

std::vector的move构造函数可以优化这一点。它可以这样做,因为内存可以从obj窃取。在std::vector中,这样做实际上很有用。

vector(vector<T> && obj) {
    this->data_ptr = obj.data_ptr;
    obj.data_ptr = nullptr;
    obj.size = 0;
}

除了将移出的observer_ptr归零对性能的影响较小之外,这很容易解决(复制而不是移动),主要原因可能是尽可能地模仿常规指针的行为,遵循最小意外原则。

然而,有一个潜在的更重要的性能问题:默认的move函数允许observer_ptr是可复制的。普通可复制性允许使用std::memcpy复制对象。如果observer_ptr不是可复制的,那么具有observer_ptr数据成员的任何类也不会,这会导致性能损失,并向下级联到组合类层次结构。

我不知道使用std::memcpy优化可以获得什么样的性能改进,但它们可能比前面提到的小性能问题更重要。