Move which throws?
Asked Answered
K

3

12

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.

Kaiserslautern answered 7/11, 2013 at 10:16 Comment(4)
I was wondering the same for move. But why nothrow swap?Ecosystem
@BЈовић: You need a non-throwing swap to get a strong exception guarantee from copy-and-swap and similar idioms.Delanty
Stream objects' move constructors can throw.Underlie
@Underlie why would they throw?Ecosystem
N
11

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.

Narcho answered 7/11, 2013 at 18:38 Comment(6)
Hehe, come to think about it, I've written a Copy-On-Write class myself which also has a throwing move as it always has to have an object allocated.Kaiserslautern
Hmm, still no swap that throws thouKaiserslautern
But why would you need to allocate a new object if you can just steal the one you're moving from? Unless you want to leave the moves from object in a valid state that is, but do you really need to do that?Thrift
I didn't find these examples very convincing. For the std::list it's possible to move the dynamically allocated node as well. There is no need to allocate on move unless you want to keep the moved-from object in some zombie (partially usable) state. The other one is a programmer error (can't assign an incompatible allocator), and deserves std::terminate().Donnadonnamarie
@ValentinMilea: Feel free to take your designs to the implementors of Visual Studio and gcc: howardhinnant.github.io/container_summaryNarcho
@ValentinMilea: You should read up on propagate_on_container_move_assignment before spreading misinformation. Here is a convenience link for you: open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4527.pdfNarcho
D
9

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

Discarnate answered 7/11, 2013 at 18:42 Comment(0)
J
1

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

Jocko answered 13/9, 2018 at 16:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.