Search code examples
c++c++11movemove-semanticsstdmove

move or copy when passing arguments to the constructor and member functions


The following is an example of my typical code. A have a lot of objects that look like this:

struct Config
{
    Config();
    Config(const std::string& cType, const std::string& nType); //additional variables omitted
    Config(Config&&) = default;
    Config& operator=(Config&&) = default;

    bool operator==(const Config& c) const;
    bool operator!=(const Config& c) const;

    void doSomething(const std::string& str);
    bool doAnotherThing(const MyOtherObject& obj);
    void doYetAnotherThing(int value1, unsigned long value2, const std::string& value3, MyEnums::Seasons value4, const std::vector<MySecondObject>& value5);

    std::string m_controllerType;
    std::string m_networkType;
    //...
};

//...

Config::Config(const std::string& cType, const std::string& nType) :
    m_controllerType(cType),
    m_networkType(nType)
{
}

My motivations and general understand of the subject:

  • use const references in constructors and methods to avoid double-copying when passing objects.
  • simple types - pass by value; classes and structs - pass by const reference (or simple reference when I need to modify them)
  • force compiler to create default move constructor and move assignment so that It would be able to do it's fancy magic and simultaneously it allows to avoid writing boring ctor() : m_v1(std::move(v1)), m_v2(std::move(v2)), m_v3(std::move(v3)) {}.
  • if it performs badly, use libc and raw pointers, then wrap it at class and write a comment.

I have a strong feeling that by rules of thumb are flawed and simply incorrect.

After reading cppreference, Scott Mayers, C++ standard, Stroustrup and so on, I feel like: "Yea, I understand every word here, but it still doesn't make any sense'. The only thing I king of understood is that move semantics makes sense when my class contains non-copiable types, like std::mutex and std::unique_ptr.

I've seen a lot of code where people pass complex object by value, like large strings, vectors and custom classes - I believe this is where move semantics happen, but, again, how can you pass an object to a function by move? If I am correct, it would leave an object in a "kind-of-null-state", making it unusable.

So, the questionы are: - How do I correctly decide between pass-by-value and pass-by-reference? - Do I need to provide both copy and move constructors? - Do I need to explicitly write move and copy constructors? May I use = default? My classes are mostly POD object so there is no complex login involved. - When debugging, I can always write std::cout << "move\n"; or std::cout << "copy\n"; in constructors of my own classes, but how do I know what happens with classes from stdlib?

P.S. It may look like it is a cry out of desperation (it is), not a valid SO question. I simply don't know to formulate my problems better than this.


Solution

    • If it is a primitive type, pass by value. Locality of reference wins.

    • If you aren't going to store a copy of it, pass by value or const&.

    • If you want to store a copy of it, and it is very cheap to move and modestly expensive to copy, pass by value.

    • If something has a modest cost to move, and is a sink parameter, consider pass by rvalue reference. Users will be forced to std::move.

    • Consider providing a way for callers to emplace construct into the field in highly generic code, or where you need every ounce of performance

    The Rule of 0/3/5 describes how you should handle copy assign/construct/destroy. Ideally you follow the rule of 0; copy/move/destruct is all =default in anything except resource management types. If you want to implement any of copy/move/destruct, you need to implement, =default or =delete every other one of the 5.

    If you are only taking 1 argument to a setter, consider writing both the && and const& versions of the setter. Or just exposing the underlying object. Move-assignment sometimes reuses storage and that is efficient.

    Emplacing looks like this:

    struct emplace_tag {};
    struct wrap_foo {
      template<class...Ts>
      wrap_foo(emplace_tag, Ts&&...ts):
        foo( std::forward<Ts>(ts)... )
      {}
      template<class T0, class...Ts>
      wrap_foo(emplace_tag, std::initializer_list<T0> il, Ts&&...ts):
        foo( il, std::forward<Ts>(ts)... )
      {}
    private:
      Foo foo;
    };
    

    there are a myriad of other ways you can permit "emplace" construction. See emplace_back or emplace in standard containers as well (where they use placement ::new to construct objects, forwarding objects passed in).

    Emplace construct even permits direct construction without even a move using objects with an operator T() setup properly. But that is something that is beyond the scope of this question.