【问题标题】:Would it be reasonable to define destruction order of vector elements?定义向量元素的破坏顺序是否合理?
【发布时间】:2012-06-20 13:27:42
【问题描述】:

我知道向量元素的破坏顺序不是由 C++ 标准定义的(参见Order of destruction of elements of an std::vector),我看到我检查的所有编译器都从头到尾进行了这种破坏——这让我很惊讶,因为动态和静态数组确实如此它以相反的顺序,而这种相反的顺序在 C++ 世界中很常见。

严格地说:我知道“容器成员......可以使用例如插入和擦除成员函数以任何顺序构造和销毁”并且我不投票支持“容器在这些更改上保留某种日志”。我只会投票支持将当前的向量析构函数实现从前向破坏更改为元素的后向破坏——仅此而已。并且可能将此规则添加到 C++ 标准中。

为什么?这样从数组转换为向量会更安全。

真实世界的例子: 我们都知道互斥锁的锁定和解锁顺序非常重要。并确保解锁发生 - 使用 ScopeGuard 模式。那么销毁顺序很重要。考虑这个例子。那里 - 从数组切换到向量会导致死锁 - 只是因为它们的破坏顺序不同:

class mutex {
public:
    void lock() { cout << (void*)this << "->lock()\n"; }
    void unlock() { cout << (void*)this << "->unlock()\n"; }
};

class lock {
    lock(const mutex&);
public:
    lock(mutex& m) : m_(&m) { m_->lock(); }
    lock(lock&& o) { m_ = o.m_; o.m_ = 0; }
    lock& operator = (lock&& o) { 
        if (&o != this) {
            m_ = o.m_; o.m_ = 0;
        }
        return *this;
    }
    ~lock() { if (m_) m_->unlock(); }  
private:
    mutex* m_;
};

mutex m1, m2, m3, m4, m5, m6;

void f1() {
    cout << "f1() begin!\n";
    lock ll[] = { m1, m2, m3, m4, m5 };
    cout <<; "f1() end!\n";
}

void f2() {
    cout << "f2() begin!\n";
    vector<lock> ll;
    ll.reserve(6); // note memory is reserved - no re-assigned expected!!
    ll.push_back(m1);
    ll.push_back(m2);
    ll.push_back(m3);
    ll.push_back(m4);
    ll.push_back(m5);
    cout << "f2() end!\n";
}

int main() {
    f1();
    f2();
}

OUTPUT - 查看销毁顺序从 f1() 到 f2()

f1() begin!
0x804a854->lock()
0x804a855->lock()
0x804a856->lock()
0x804a857->lock()
0x804a858->lock()
f1() end!
0x804a858->unlock()
0x804a857->unlock()
0x804a856->unlock()
0x804a855->unlock()
0x804a854->unlock()
f2() begin!
0x804a854->lock()
0x804a855->lock()
0x804a856->lock()
0x804a857->lock()
0x804a858->lock()
f2() end!
0x804a854->unlock()
0x804a855->unlock()
0x804a856->unlock()
0x804a857->unlock()
0x804a858->unlock()

【问题讨论】:

  • 恕我直言,如果软件设计良好,销毁顺序应该无关紧要。当调用析构函数时,这意味着对象不再使用或不再需要。在销毁对象之前,您应该确保对象处于一致状态(在这种情况下不再使用)。
  • 我们也都知道,当执行顺序如此重要时,将它们放在任何容器中并让生成的代码破坏并不是一个好主意。 // 讽刺通知:我对“我们都知道”的说法有点怀疑
  • 可能你没有阅读就回答了。 n ScopeGuard (stackoverflow.com/questions/48647/…) 我在这里使用,销毁顺序很重要。这就是我使用这个例子的原因。
  • @user1463922:我知道 RAII 和范围守卫。我只是说,当订单很重要时,你应该自己销毁这些条目,从容器中弹出它们。
  • 具体来说“现实世界的例子”:按不同的顺序解锁怎么可能造成死锁?解锁(和 try_lock)永远不会阻塞,因此不可能参与死锁,死锁定义为阻塞循环。我错过了什么吗?

标签: c++ vector standards destruction


【解决方案1】:

我认为这是 C++ 的另一个案例,它让编译器编写者能够灵活地为其架构编写性能最高的容器。在 0.001% 的情况下,为了方便起见,要求按特定顺序销毁可能会损害性能(我实际上从未见过默认顺序不合适的另一个示例)。在这种情况下,由于vector 是连续数据,我指的是硬件能够智能地利用前瞻缓存,而不是向后迭代并可能反复丢失缓存。

如果您的容器实例需要特定的破坏顺序,该语言会要求您自己实现它以避免潜在地惩罚标准功能的其他客户端。

【讨论】:

  • 感谢您的回答。我真的认为前向和后向破坏之间没有 PERFORMANCE 差异。
  • 马克 - 所以根据你的回答 - C++ 规则在数组和成员变量中以相反的顺序调用析构函数 - 导致这种破坏不像前向那样有效?
  • @user1463922 可能会影响性能,正确。当然,对于成员变量来说,最好有一个固定的构造/销毁顺序。对于容器,情况和选择不同。
  • 我怀疑这个解释;如果定义了“c-array”破坏顺序,而“std::vector”破坏顺序未定义(出于性能原因),这是否意味着破坏“c-array”将比“std::vector”慢?
【解决方案2】:

Fwiw,libc++ 输出:

f1() begin!
0x1063e1168->lock()
0x1063e1169->lock()
0x1063e116a->lock()
0x1063e116b->lock()
0x1063e116c->lock()
f1() end!
0x1063e116c->unlock()
0x1063e116b->unlock()
0x1063e116a->unlock()
0x1063e1169->unlock()
0x1063e1168->unlock()
f2() begin!
0x1063e1168->lock()
0x1063e1169->lock()
0x1063e116a->lock()
0x1063e116b->lock()
0x1063e116c->lock()
f2() end!
0x1063e116c->unlock()
0x1063e116b->unlock()
0x1063e116a->unlock()
0x1063e1169->unlock()
0x1063e1168->unlock()

它是故意以这种方式实现的。定义here的关键函数是:

template <class _Tp, class _Allocator>
_LIBCPP_INLINE_VISIBILITY inline
void
__vector_base<_Tp, _Allocator>::__destruct_at_end(const_pointer __new_last, false_type) _NOEXCEPT
{
    while (__new_last != __end_)
        __alloc_traits::destroy(__alloc(), const_cast<pointer>(--__end_));
}

只要size() 需要收缩,就会调用这个私有实现细节。

我还没有收到任何关于这个可见的实施细节的反馈,无论是正面的还是负面的。

【讨论】:

  • 很高兴知道存在这样的实现。
  • 您如何看待 Mark 给出的前向销毁的基本原理:使用“硬件智能地利用前瞻缓存的能力,而不是向后迭代并可能反复丢失缓存”。 libc++号称性能不错……
  • 这是一种可能性。但我没有注意到这样的差异。我也没有找过。我的任何客户也没有抱怨这方面的性能问题(我收到了其他方面的性能投诉 - 有些已经解决,有些仍在我的待办事项清单上)。
  • @PiotrNycz 自 P4 以来的英特尔 CPU 执行正向和反向预取。 software.intel.com/en-us/articles/…
猜你喜欢
  • 2011-09-04
  • 1970-01-01
  • 2016-01-18
  • 2019-12-02
  • 2016-11-09
  • 1970-01-01
  • 2017-02-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多