Namespaces
Variants

Template talk:cpp/container/constructor

From cppreference.com

Should there not be the prototypes for the pre C++14 constructors? --Ybab321 (talk) 14:44, 1 July 2017 (PDT)

vector move does NOT leave source empty

… according to the standard, a moved-from vector is left in a valid but unspecified state ([lib.types.movedfrom], there are no overriding clauses for containers, sequence containers or vector) in particular.

The change was made by user Cubbi, and a source comment was also added: "indirectly: due to allocator propagation and by virtue of being noexcept"

However, the conclusion does not follow from the premises. In fact, it can easily be disproved with the following two hypothetical implementations:

vector class definition (excerpt and simplified to use new/delete):

template<typename T>
class vector
{
    T *begin_, *end_, *capEnd_;

// ...
public:
    // ...
    vector(vector &&from);
    // ...
};

Example c'tor definition A:

template<typename T>
vector<T>::vector(vector &&from)
    : begin_(from.begin_)
    , end_(from.end_)
    , capEnd_(from.capEnd_)
{
    from.begin_ = from.end_ = static_cast<T *>(static_cast<void *>(new (nothrow) unsigned char[size() * sizeof(T)]));
    if ( from.begin_ != nullptr )
    {
        from.capEnd_ = from.begin_ + size();
        try
        {
            while ( from.end_ != from.capEnd_ )
            {
                new (from.begin_ + size()) T();
                ++from.end_;
            }
        }
        catch (...)
        {
        }
    }
    else
    {
        from.capEnd_ = nullptr;
    }
}

This requires a default-c'tible T, but this can be solved by conditionally enabling the code with the use of type traits. (I only need to prove that the "empty guarantee" claim is false for one single combination of T and Allocator.)

Example B, which is a lot more obvious and might even have been in use at some point:

template<typename T>
vector<T>::vector(vector &&from)
{
    swap(from);
}

Example A at least makes no practical sense, but the point is simply to show that the reasoning from the comment is a non sequitur. A conforming implementation can have surprising behavior, if invalid assumptions are made about the guarantees provided by the language standard.

The two clauses relating to the non-existing guarantee should therefore be removed. Note that this article has been cited in this recent discussion on stackoverflow to support an argument that move constructor and move assignment operators in the C++ standard library "usually" provide a guarantee that the source is left as empty, which to the best of my knowledge only actually holds for `std::unique_ptr`, `std::shared_ptr` and (as of C++14) `std::weak_ptr`.

@Cubbi I am not removing the clauses for now because you linked (to support your argument) an e-mail from an archive of a closed list, which I have no access to. Maybe you could reproduce the reasoning here? Arvo (talk) 06:28, 13 August 2018 (PDT)

To summarize the WG21 thread I linked to
  • [1] X u(rv) indirectly guarantees that rv.empty() is true afterwards, because since std::vector doesn't allow a "Small-Vector Optimization" like std::string's Small-String Optimization, rv can't have any storage left after that move. It must transfer what it owns to u, and cannot allocate new storage (because the move constructor is noexcept). This guarantee doesn't exist for X u(rv, alloc) because if alloc != rv.get_allocator() then you will allocate new storage using alloc, instead of taking ownership of rv's storage.
  • [2] A container move construction has constant complexity for everything but std::array, so we know exactly the state of rv after the operation, and it must be empty, especially because we know that the type-invariants of rv must hold - the operations that have no preconditions remain valid, and the only way for that to be the case after a constant-complexity move is that rv is empty. The only exception is an SSO std::string, which can perform a move in constant time doing a small, bounded-size memcpy.
--Cubbi (talk)
PS, I see Howard Hinnant commented on the SO thread you linked with a link to his own exposition at [3] --Cubbi (talk) 08:34, 13 August 2018 (PDT)
I had other things to do and then forgot about this issue but came back to it because I saw this talk with the same type of misinformation – admittedly worse because leaving the original string unchanged can be a reasonable optimization for SSO.
The claim that new memory cannot be allocated because of noexcept is simply wrong, as my example shows. Did you actually read it? I can do new (std::nothrow) T[capacity] instead of calling std::allocator<T>::allocate() or just put the allocator call in a try{/*...*/} catch(...){ } . If the allocation fails, I can leave the source object empty.
As for the complexity argument, there is a problem in my example in that it has linear complexity in general. However, this is easily resolved by, for instance, allocating an array of bounded size or only doing it for POD types (using std::is_trivially_default_constructible<T>), I guess).
Note that I do not have to prove that any of this makes practical sense or is generalizable to all value types and allocator types. You made a universal positive claim. All I need is one counterexample and your claim is disproved. We can limit ourselves to only one container, value type and allocator in fact. std::vector<bool> might be a poor example for other reasons, but std::vector<int> works fine. So, I can simplify (and fix, as explained above) my example constructor to:
std::vector<int>::vector(std::vector<int>&& from)
    : begin_(from.begin_)
    , end_(from.end_)
    , capEnd_(from.capEnd_)
{
    from.begin_ = new (std::nothrow) int[1];
    from.end_ = from.capEnd_ = from.begin_ ? from.begin_ + 1 : nullptr;
    // in general no need to initialize *from.end_ although a user library would be obliged to
    // to avoid UB
}
Arvo (talk) 05:20, 16 October 2020 (PDT)
It's not my claim, it's the claim made by the people who wrote the spec. If you change your example to call allocate and swallow bad_alloc (new is against the spec), I suppose you could claim that you have a malicious vector with sometimes-nonempty moved-from state. If you feel strongly about it, file a defect report to get it specified explicitly. Since cppreference covers things that actually exist or are intended to exist, it is correct to say it's empty. --Cubbi (talk) 14:31, 16 October 2020 (PDT)