When to make a type non-movable in C++11?

前端 未结 4 1682
耶瑟儿~
耶瑟儿~ 2020-11-29 15:40

I was surprised this didn\'t show up in my search results, I thought someone would\'ve asked this before, given the usefulness of move semantics in C++11:

When do I

相关标签:
4条回答
  • 2020-11-29 16:23

    Short answer: If a type is copyable, it should also be moveable. However, the reverse is not true: some types like std::unique_ptr are moveable yet it doesn't make sense to copy them; these are naturally move-only types.

    Slightly longer answer follows...

    There are two major kinds of types (among other more special-purpose ones such as traits):

    1. Value-like types, such as int or vector<widget>. These represent values, and should naturally be copyable. In C++11, generally you should think of move as an optimization of copy, and so all copyable types should naturally be moveable... moving is just an efficient way of doing a copy in the often-common case that you don't need the original object any more and are just going to destroy it anyway.

    2. Reference-like types that exist in inheritance hierarchies, such as base classes and classes with virtual or protected member functions. These are normally held by pointer or reference, often a base* or base&, and so do not provide copy construction to avoid slicing; if you do want to get another object just like an existing one, you usually call a virtual function like clone. These do not need move construction or assignment for two reasons: They're not copyable, and they already have an even more efficient natural "move" operation -- you just copy/move the pointer to the object and the object itself doesn't have to move to a new memory location at all.

    Most types fall into one of those two categories, but there are other kinds of types too that are also useful, just rarer. In particular here, types that express unique ownership of a resource, such as std::unique_ptr, are naturally move-only types, because they are not value-like (it doesn't make sense to copy them) but you do use them directly (not always by pointer or reference) and so want to move objects of this type around from one place to another.

    0 讨论(0)
  • 2020-11-29 16:23

    Another reason I've found - performance. Say you have a class 'a' which holds a value. You want to output an interface which allows a user to change the value for a limited time (for a scope).

    A way to achieve this is by returning a 'scope guard' object from 'a' which sets the value back in its destructor, like so:

    class a 
    { 
        int value = 0;
    
      public:
    
        struct change_value_guard 
        { 
            friend a;
          private:
            change_value_guard(a& owner, int value) 
                : owner{ owner } 
            { 
                owner.value = value;
            }
            change_value_guard(change_value_guard&&) = delete;
            change_value_guard(const change_value_guard&) = delete;
          public:
            ~change_value_guard()
            {
                owner.value = 0;
            }
          private:
            a& owner;
        };
    
        change_value_guard changeValue(int newValue)
        { 
            return{ *this, newValue };
        }
    };
    
    int main()
    {
        a a;
        {
            auto guard = a.changeValue(2);
        }
    }
    

    If I made change_value_guard movable, I'd have to add an 'if' to its destructor that would check if the guard has been moved from - that's an extra if, and a performance impact.

    Yeah, sure, it can probably be optimized away by any sane optimizer, but still it's nice that the language (this requires C++17 though, to be able to return a non-movable type requires guaranteed copy elision) does not require us to pay that if if we're not going to move the guard anyway other than returning it from the creating function (the dont-pay-for-what-you-dont-use principle).

    0 讨论(0)
  • 2020-11-29 16:27

    Actually when I search around, I found quite some types in C++11 are not movable:

    • all mutex types(recursive_mutex , timed_mutex, recursive_timed_mutex,
    • condition_variable
    • type_info
    • error_category
    • locale::facet
    • random_device
    • seed_seq
    • ios_base
    • basic_istream<charT,traits>::sentry
    • basic_ostream<charT,traits>::sentry
    • all atomic types
    • once_flag

    Apparently there is a discussion on Clang: https://groups.google.com/forum/?fromgroups=#!topic/comp.std.c++/pCO1Qqb3Xa4

    0 讨论(0)
  • 2020-11-29 16:30

    Herb's answer (before it was edited) actually gave a good example of a type which shouldn't be movable: std::mutex.

    The OS's native mutex type (e.g. pthread_mutex_t on POSIX platforms) might not be "location invariant" meaning the object's address is part of its value. For example, the OS might keep a list of pointers to all initialized mutex objects. If std::mutex contained a native OS mutex type as a data member and the native type's address must stay fixed (because the OS maintains a list of pointers to its mutexes) then either std::mutex would have to store the native mutex type on the heap so it would stay at the same location when moved between std::mutex objects or the std::mutex must not move. Storing it on the heap isn't possible, because a std::mutex has a constexpr constructor and must be eligible for constant initialization (i.e. static initialization) so that a global std::mutex is guaranteed to be constructed before the program's execution starts, so its constructor cannot use new. So the only option left is for std::mutex to be immovable.

    The same reasoning applies to other types that contain something that requires a fixed address. If the address of the resource must stay fixed, don't move it!

    There is another argument for not moving std::mutex which is that it would be very hard to do it safely, because you'd need to know that noone is trying to lock the mutex at the moment it's being moved. Since mutexes are one of the building blocks you can use to prevent data races, it would be unfortunate if they weren't safe against races themselves! With an immovable std::mutex you know the only things anyone can do to it once it has been constructed and before it has been destroyed is to lock it and unlock it, and those operations are explicitly guaranteed to be thread safe and not introduce data races. This same argument applies to std::atomic<T> objects: unless they could be moved atomically it wouldn't be possible to safely move them, another thread might be trying to call compare_exchange_strongon the object right at the moment it's being moved. So another case where types should not be movable is where they are low-level building blocks of safe concurrent code and must ensure atomicity of all operations on them. If the object value might be moved to a new object at any time you'd need to use an atomic variable to protect every atomic variable so you know if it's safe to use it or it's been moved ... and an atomic variable to protect that atomic variable, and so on...

    I think I would generalize to say that when an object is just a pure piece of memory, not a type which acts as a holder for a value or abstraction of a value, it doesn't make sense to move it. Fundamental types such as int can't move: moving them is just a copy. You can't rip the guts out of an int, you can copy its value and then set it to zero, but it's still an int with a value, it's just bytes of memory. But an int is still movable in the language terms because a copy is a valid move operation. For non-copyable types however, if you don't want to or can't move the piece of memory and you also can't copy its value, then it's non-movable. A mutex or an atomic variable is a specific location of memory (treated with special properties) so doesn't make sense to move, and is also not copyable, so it's non-movable.

    0 讨论(0)
提交回复
热议问题