Pergunta

The idiomatic way to implement move-operations on classes with a standard container member can not be noexcept and therefore will not be movable by operations like vector.push_back(). Or am I mistaken?

To get speed from

vector<Elem> data;
// ...
data.push_back( elem );

We are encouraged to make out move operations noexcept -- so during the vectors resize the library can safely move elements to the reallocated storage.

class Elem {
    // ...
    Elem(Elem&&) noexcept;            // noexcept important for move
    Elem& operator=(Elem&&) noexcept; // noexcept important for move
};

So far so good, now my elems can be pushed-back much faster.

But: If I add a container as member, can my class be still be marked noexcept-move? All standard containers do not have their move noexcept!

class Stuff {
    vector<int> bulk;
    // ...
    Stuff(Stuff&& o)  // !!! no noexcept because of vector-move  
      : bulk(move(o.bulk))
      {}
    Stuff& operator=(Stuff&&) // !!! no noexcept...
      { /* appropriate implementation */ }
};

This also means, that we can also not rely on the compiler-generated move-operations, right? The following complete class will also not have noexcept-move-operations and therefore not be "fast", correct?

struct Holder {
    vector<int> bulk;
};

Maybe vector<int> is a bit too simple to move, but what about vector<Elem>?

This would have great consequences on all data structures with containers as members.

Foi útil?

Solução

I feel your pain, really.

Some std::implementations will mark the move members of containers as noexcept, at least conditional on the allocator properties, as an extension. You can adapt your code to automatically take advantage of these extensions, for example:

class Stuff {
    std::vector<int> bulk;
    // ...
public:
    Stuff(Stuff&& o)
      noexcept(std::is_nothrow_move_constructible<std::vector<int>>::value)
      : bulk(std::move(o.bulk))
      {}
    Stuff& operator=(Stuff&&)
      noexcept(std::is_nothrow_move_assignable<std::vector<int>>::value)
      { /* appropriate implementation */ }
};

And you can even test whether or not your type does have noexcept move members:

static_assert(std::is_nothrow_move_constructible<Stuff>::value,
                     "I hope Stuff has noexcept move members");
static_assert(std::is_nothrow_move_assignable<Stuff>::value,
                     "I hope Stuff has noexcept move members");

libc++ in particular does have noexcept move members for all of its containers, whenever the allocator allows, and std::allocator always allows the container move members to be noexcept.

Outras dicas

It depends on the allocator that your standard container is using. The default allocator std::allocator<T> is guaranteed not to throw on copy (and has no move constructor), which in turn means that the container will not throw on move.

One interesting feature of noexcept compared with the deprecated throw() is that you can use an expression that is evaluated at compile time. The exact condition to be tested might not be trivial though...

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top