0

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.

CorellianAle
  • 645
  • 8
  • 16

1 Answers1

2
  • 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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • I have read that "take by value and move when you want a copy" is not necessarily the best option. [Further reading](https://stackoverflow.com/questions/26261007/why-is-value-taking-setter-member-functions-not-recommended-in-herb-sutters-cpp). But overall this is solid advice. – Max Langhof Jun 27 '18 at 14:17
  • 1
    @MaxLanghof That link is for setters, not constructors. There is no preexisting storage for constructors. Which is mostly what I'm talking about above (note I say that assignment should almost always be just =defaulted). – Yakk - Adam Nevraumont Jun 27 '18 at 14:20
  • "Consider providing a way for callers to emplace construct into the field in highly generic code, or where you need every ounce of performance" May you explain it a bit more? With an example, if possible. – CorellianAle Jun 27 '18 at 14:21
  • 1
    @MaxLanghof But this question goes beyond construction, so I've addressed it now. – Yakk - Adam Nevraumont Jun 27 '18 at 14:26