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:
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)) {}
.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.
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.