Viktor Sehr
Viktor Sehr

Reputation: 13099

Move which throws?

To my understanding, move-constructors and move-assign must be marked noexcept in order for the compiler to utilize them when, for example, reallocating inside a vector.

However, is there any real-world case where a move-assign, move-construct might actually throw?

Update:

Classes that for example has an allocated resource when constructed cant be no-throw move.

Upvotes: 12

Views: 3170

Answers (3)

KeyC0de
KeyC0de

Reputation: 5267

Move constructors on classes with const data members can also throw. Check here for more info.

Upvotes: 1

Howard Hinnant
Howard Hinnant

Reputation: 218770

However, is there any real-world case where a move-assign, move-construct (or swap) might actually throw?

Yes. Consider an implementation of std::list. The end iterator must point "one past the last element" in the list. There exist implementations of std::list where what end points to is a dynamically allocated node. Even the default constructor allocates such a node so that when you call end(), there is something to point to.

In such an implementation, every constructor must allocate a node for end() to point to… even the move constructor. That allocation may fail, and throw an exception.

This same behavior can extend to any node-based container.

There are also implementations of these node-based containers that do a "short-string" optimization: They embed the end node within the container class itself, instead of dynamically allocating. Thus the default constructor (and move constructor) need not allocate anything.

The move assignment operator can throw for any container<X> if for the container's allocator propagate_on_container_move_assignment::value is false, and if the allocator in the lhs is not equal to the allocator in the rhs. In that case the move assignment operator is forbidden from transferring memory ownership from the rhs to the lhs. This can not happen if you are using std::allocator, as all instances of std::allocator are equal to one another.

Update

Here is a conforming and portable example of the case when propagate_on_container_move_assignment::value is false. It has been tested against the latest version of VS, gcc and clang.

#include <cassert>
#include <cstddef>
#include <iostream>
#include <vector>

template <class T>
class allocator
{
    int id_;
public:
    using value_type    = T;

    allocator(int id) noexcept : id_(id) {}
    template <class U> allocator(allocator<U> const& u) noexcept : id_(u.id_) {}

    value_type*
    allocate(std::size_t n)
    {
        return static_cast<value_type*>(::operator new (n*sizeof(value_type)));
    }

    void
    deallocate(value_type* p, std::size_t) noexcept
    {
        ::operator delete(p);
    }

    template <class U, class V>
    friend
    bool
    operator==(allocator<U> const& x, allocator<V> const& y) noexcept
    {
        return x.id_ == y.id_;
    }
};

template <class T, class U>
bool
operator!=(allocator<T> const& x, allocator<U> const& y) noexcept
{
    return !(x == y);
}

template <class T> using vector = std::vector<T, allocator<T>>;

struct A
{
    static bool time_to_throw;

    A() = default;
    A(const A&) {if (time_to_throw) throw 1;}
    A& operator=(const A&) {if (time_to_throw) throw 1; return *this;}
};

bool A::time_to_throw = false;

int
main()
{
    vector<A> v1(5, A{}, allocator<A>{1});
    vector<A> v2(allocator<A>{2});
    v2 = std::move(v1);
    try
    {
        A::time_to_throw = true;
        v1 = std::move(v2);
        assert(false);
    }
    catch (int i)
    {
        std::cout << i << '\n';
    }
}

This program outputs:

1

which indicates that the vector<T, A> move assignment operator is copy/moving its elements when propagate_on_container_move_assignment::value is false and the two allocators in question do not compare equal. If any of those copies/moves throws, then the container move assignment throws.

Upvotes: 11

Alexandre C.
Alexandre C.

Reputation: 56956

Yes, throwing move constructors exist in the wild. Consider std::pair<T, U> where T is noexcept-movable, and U is only copyable (assume that copies can throw). Then you have a useful std::pair<T, U> move constructor which may throw.

There is a std::move_if_noexcept utility in the standard library if you need (useful to implement std::vector::resize with at least the basic exception guarantee).

See also Move constructors and the Strong Exception Guarantee

Upvotes: 9

Related Questions