11

I know that vector elements destruction order is not defined by C++ standard (see Order of destruction of elements of an std::vector) and I saw that all compilers I checked do this destruction from begin to end - which is quite surprising to me since dynamic and static arrays do it in reverse order, and this reverse order is quite often in C++ world.

To be strict: I know that "Container members ... can be constructed and destroyed in any order using for example insert and erase member functions" and I do not vote for "containers to keep some kind of log over these changes". I would just vote for changing current vector destructor implementation from forward destruction to backward destruction of elements - nothing more. And maybe add this rule to C++ standard.

And the reason why? The changing from arrays to vector would be safer this way.

REAL WORLD EXAMPLE: We all know that mutexes locking and unlocking order is very important. And to ensure that unlocking happens - ScopeGuard pattern is used. Then destruction order is important. Consider this example. There - switching from arrays to vector causes deadlock - just because their destruction order differs:

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 - see the destruction order change from f1() to 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()
Community
  • 1
  • 1
PiotrNycz
  • 20,687
  • 7
  • 55
  • 102
  • 1
    IMHO the destruction order should not matter if the software is well designed. When a destructor is called, this means the objects are no longer in use or required. You should make sure your objects are in a consistent state (in this case not used anymore) before destroying them. – m0skit0 Jun 18 '12 at 14:43
  • We also all know that when order of executing is that important, that it is not a good idea to put them in any container and let generated code destroy. // sarcasm notice: I'm a bit suspicious with "we all know" statements – stefaanv Jun 18 '12 at 14:53
  • Probably you answered without reading. n ScopeGuard (http://stackoverflow.com/questions/48647/does-scopeguard-use-really-lead-to-better-code) I used here, the destruction order matters. That's why I used this example. – PiotrNycz Jun 18 '12 at 14:55
  • @user1463922: I know RAII and scopeguards. I just mean that when order matters, you should destroy the entries yourself, by popping them from the container. – stefaanv Jun 18 '12 at 15:32
  • stefaanv: I did not answer to your comment. Frankly I answered to m0skit0 and you are were quick enough to take the place between us. BTW, you mean popping in new vector specialization destructor... I thought about resize(0) I've heard somewhere that resize() shrinks in reverse order and standard tells that. – PiotrNycz Jun 18 '12 at 16:11
  • 1
    Regarding the "real world example" specifically: How can unlocking in different orders possibly create a deadlock? Unlock (and try_lock) never block, so cannot possibly participate in a deadlock which is by definition a blocking cycle. Am I missing something? – Herb Sutter Nov 03 '17 at 13:39
  • @HerbSutter I think you are right and I did mistake. I just used the fact that locking order is important and w/o really thinking much about it - I show that changing container from array (inluding std::array) to vector might change unlocking order. The real real world example might contain list of subobjects that depend on each other and when destructed previously constructed subobject is called by some subject that was constructed later. When I found a minute I'll try to change this question. – PiotrNycz Nov 03 '17 at 17:57

2 Answers2

5

I think this is another case of C++ giving compiler writers the flexibility to write the most performant containers for their architecture. Requiring destruction in a particular order could hurt performance for a convenience in something like 0.001% of cases (I've actually never seen another example where the default order wasn't suitable). In this case since vector is contiguous data I'm referring to the hardware's ability to utilize look-ahead caching intelligently instead of iterating backwards and probably repeatedly missing the cache.

If a particular order of destruction is required for your container instance, the language asks that you implement it yourself to avoid potentially penalizing other clients of the standard features.

Mark B
  • 91,641
  • 10
  • 102
  • 179
  • Thanks for this answer. I really thought that there is no PERFORMANCE difference between forward and backward destruction. – PiotrNycz Jun 18 '12 at 15:33
  • Mark - so according to your answer - C++ rule to calls destructors in reverse orders in arrays and member variables - causes that this destruction is not such efficient as this in forward order? – PiotrNycz Jun 18 '12 at 16:15
  • @user1463922 There could possibly be performance implications, correct. Certainly for member variables however it's desirable to have a fixed order of construction/destruction. For containers it's a different case and choice. – Mark B Jun 18 '12 at 16:25
  • I'm dubious of this explanation; if 'c-array' destruction order is defined, and 'std::vector' destruction order is not (for performance reasons), does that mean that destroying a 'c-array' will be slower than a 'std::vector'? – Charles L Wilcox Sep 24 '16 at 06:43
4

Fwiw, libc++ outputs:

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()

It was purposefully implemented this way. The key function defined here is:

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_));
}

This private implementation-detail is called whenever the size() needs to shrink.

I have not yet received any feedback on this visible implementation detail, either positive or negative.

Howard Hinnant
  • 179,402
  • 46
  • 391
  • 527
  • 1
    It nice to know that there exists such implementation. – PiotrNycz Jun 19 '12 at 21:00
  • what do you think about rationale given by Mark for forward destruction: to use "hardware's ability to utilize look-ahead caching intelligently instead of iterating backwards and probably repeatedly missing the cache." libc++ claims to be good in performance... – PiotrNycz Jun 19 '12 at 21:13
  • This is a possibility. But I have not noticed such a difference. Nor have I looked for it. Nor have any of my customers complained of a performance problem in this area (I have gotten performance complaints in other areas - some already addressed, some still on my to-do list). – Howard Hinnant Jun 19 '12 at 22:32
  • @PiotrNycz Intel CPUs since the P4 do forward and backward prefetching. http://software.intel.com/en-us/articles/optimizing-application-performance-on-intel-coret-microarchitecture-using-hardware-implemented-prefetchers/ – Charles L Wilcox Sep 24 '16 at 06:50