Skip to content

Latest commit

 

History

History
56 lines (39 loc) · 4.83 KB

File metadata and controls

56 lines (39 loc) · 4.83 KB

notes about c++ move semantics

  • before c++11, assignment by value always required making a copy. however, starting c++11, we now have a difference between copying and moving: the latter is a cheaper option that simply "steals ownership" of the same data in memory, by just copying the internal pointers to the new object and setting the old object's pointers to nullptr. no copies of the actual data in memory (e.g. vector contents) are made.

  • by default (i.e., without explicitly writing std::move()), moving only happens when assigning an rvalue or "temporary" to something, as otherwise the old object would not be usable with all its pointers set to nullptr. there is also a constructor vs. an assignment operator we can manually define for both copying and moving. so, in more detail, here's a table showing exactly when each kind of operation/constructor is used:

    =============================================================================
    ‖ assigning \ assigning ‖       new object       |     existing object      ‖
    ‖   from:    \    to:   ‖                        |                          ‖
    ‖===========================================================================‖
    ‖        lvalue         ‖    copy constructor    | copy assignment operator ‖
    ‖                       ‖ (e.g. `T obj2 = obj1`) |   (e.g. `obj2 = obj1`)   ‖
    ‖---------------------------------------------------------------------------‖
    ‖        rvalue         ‖    move constructor    | move assignment operator ‖
    ‖                       ‖  (e.g. `T obj2 = f()`) |    (e.g. `obj2 = f()`)   ‖
    =============================================================================

    do note that starting in c++17, move constructors (bottom left) in the case where the rvalue is a function call are replaced by mandatory "copy elision", which also avoids a copy. this is done through return value optimization: the function must construct the return object directly in the caller's memory (yes, even in -O0).

  • explicitly, these are the function signatures of these four constructors/operations in the table above (which shall henceforth be referred to as "Things"), for a class T:

    // copy constructor
    T(const T& other);
    
    // copy assignment operator
    T& operator =(const T& other);
    
    // move constructor
    T(T&& other) noexcept;
    
    // move assignment operator
    T& operator =(T&& other) noexcept;

    (the noexcepts are important as otherwise moves may fall back to using copies, for example by many STL algorithms operating on objects of type T.)

coding guidelines

  • these four Things are, in general, automatically generated by the compiler for each class (or struct). each of these automatically generated/default Things attempt to do the following, in order:

    1. call the version of that Thing from each parent class, in the order that they were inherited from.
    2. call the version of that Thing from each member variable, in the order that they were declared/listed in the class definition.

    (this should be the same thing that default constructors do). if something invalid happens at any point of these two steps (e.g. a parent or member does not have the Thing, default or not—see later), the compiler (iirc) will implicitly delete that Thing from the class, meaning that attempting to call it will give an error.

  • so when do these default Things not generate automatically? well, if a class (or a parent of that class, via public/protected methods):

    1. manually defines any of these four Things, then all the remaining Things do not get automatically generated defaults.
    2. manually defines a destructor, then the two move Things do not get automatically generated defaults.

    (again, similarly, default constructors are not be automatically generated if any other constructor is manually defined (including a copy or move constructor!).) if you wish to call a Thing that does not get automatically generated, you MUST declare it explicitly, using = default if you want the same default behavior as if it had been automatically generated.

  • thus, if you want to manually define any one of the Things to override the automatically generated default behavior, you should almost always manually define ALL the Things, using = default for the ones you want to keep the automatically generated behavior of. (this along with the destructor is often called the c++ "rule of five".) an explicit default constructor is also often needed along with these, since defining the copy or move constructor prevents a default constructor from being automatically generated as well.

  • examples can be found in src/utils/types/i_disk_storage.h and src/utils/types/db/ ("the rule of five" methods).