c++visual-c++containersallocator

Is it allowed to call `deallocate` on a moved-from allocator (MSVC standard containers do)


When MSVC move-constructs an std::set, it also move-constructs the allocator. Later, when it destructs the moved-from set, it uses the allocator to deallocate an element. The following explains what MSVC does (see https://godbolt.org/z/4Ts7z4Tj8 for code that actually generates a trace of the allocator actions):

#include <set>
int main() {
    // Allocator actions when default-constructing s1:
    // 1. Default-construct s1's allocator
    // 2. Use s1's allocator to allocate a node
    std::set<int> s1;

    // Allocator actions when inserting into s1:
    // 3. Use s1's allocator to allocate a node
    s1.insert(1);

    {
        // Allocator actions when move-constructing s2 from s1:
        // 4. Move-construct s2's allocator from s1
        // 5. Use s2's allocator to allocate a node
        auto s2 = std::move(s1);

        // Allocator actions when s2 goes out of scope:
        // 6. Use s2's allocator to deallocate a node
        // 7. Use s2's allocator to deallocate a node
        // 8. Destruct s2's allocator
    }

    // Allocator actions when s1 goes out of scope:
    // 9. Use the s1's allocator to deallocate a node
    // 10. Destruct s1's allocator
}

Note that (4) moves-from s1's allocator, and (9) uses s1's allocator.

This seems counter-intuitive: I would not expect it to use a moved-from allocator.

It is a practical problem for me because my allocator's deallocate member delegates work to a member std::function, and MSVC crashes when invoking a moved-from std::function. (I can work around this by making the allocator's move-constructor and move-assignment operator copy the std::function rather than move it. But it is a pity because then the move operations may allocate and throw, and maybe this makes them a bit slower.)

Is MSVC's std::set implementation's use pattern allowed, or is it a bug in MSVC's standard library implementation?

(Did not find this explicitly mentioned at allocator.requirements, although Table 28 (1) states that the move-constructor "Shall not exit via an exception" and (2) has the post-condition that the moved-to object "equals the prior value of" the moved-from object. The use of the word "prior" in (2) seems to hint that the moved-from object may have changed. (1) might make my workaround invalid, although it may be OK for me to declare the move-constructor as noexcept and have it call terminate on allocation failure. I guess lib.types.movedfrom does not apply to user-defined allocators.)

(FWIW I think the observed behavior also shows that MSVC's std::set does not respect propagate_on_container_move_assignment, but my question is not about that. The behavior is identical regardless of propagate_on*.)

(Verified the behavior on both oldest and newest versions of MSVC available on godbolt.org, and both for C++11 and C++20. It seems to occur for all node-based containers. Also, in debug mode, it seems to occur for e.g. vectors because they allocate something in their default constructor.)


Solution

  • The std::allocator has no move operations, so it is always copied. That is fine, as it also holds no state, so there is nothing to move.

    Custom allocators could behave differently, but when asked about that the committee decided not to allow that. The "unchanged when copied or moved" was added to the allocator requirements.

    This is the result of library isses 2593 Moved-from state of Allocators which specifies the result of these operations

    X u(move(a));
    X u = move(a);
    

    to be "Shall not exit via an exception. Post: The value of a is unchanged and is equal to u."

    Allocators that compare equal can be used interchangeably to deallocate nodes, which was the core of the question.