I'm having trouble wrapping my brain around ownership and maximizing performance with moves. Imagine this hypothetical set of classes emulating an Excel workbook.
namespace Excel {
class Cell
{
public:
// ctors
Cell() = default;
Cell(std::string val) : m_val(val) {};
// because I have a custom constructor, I assume I need to also
// define copy constructors, move constructors, and a destructor.
// If I don't my understanding is that the private string member
// will always be copied instead of moved when Cell is replicated
// (due to expansion of any vector in which it is stored)? Or will
// it be copied, anyways (so it doesn't matter or I could just
// define them as default)
value() const { return m_val; }; // getter (no setter)
private:
std::string m_val;
}
class Row
{
public:
// ctors
Row() = default;
Row(int cellCountHint) : m_rowData(cellCountHint) {}
// copy ctors (presumably defaults will copy private vector member)
Row(const Row&) = default;
Row& operator=(Row const&) = default;
// move ctors (presumably defaults will move private vector member)
Row(Row&& rhs) = default;
Row& operator=(Row&& rhs) = default;
// and if I want to append to internal vector, might I get performance
// gains by moving in lieu of copying, since Cells contain strings of
// arbitrary length/size?
void append(Cell cell) { m_rowData.push_back(cell); };
void append(Cell &&cell) { m_rowData.push_back(std::move(cell)); };
private:
std::vector<Cell> m_rowData;
}
}
And so on:
I felt no need to implement these last two for the MWE since they are effectively duplicative of Row (my assumption is they would be identical to the design of Row).
It's confusing to me to understand if it's ok to rely on the defaults or if I should be defining my own move constructors (and not keeping them default) to ensure the private vector member variable is moved rather than copied, but this is all very confusing to me and I can only seem to find overly simplistic examples of a class with nothing but members of built-in types.
If a class deals with the ownership of a resource, then that class should ONLY manage that resource. It shouldn't do anything else. In this case define all 5 (rule of 5).
Else, a class doesn't need to implement any of the 5 (rule of 0).
Simple as that.
Now, I've seen these rules formulated as follows: if a class defines any of the 5 then it should define all of them. Well, yes, and no. The reason behind it is: if a class defines any of the 5 then that is a strong indicator that the class has to manage a resource, in which case it should define all 5. So, for instance if you define a destructor to free a resource then the class is in the first category and should implement all 5, but if you define a destructor just to add some debug statements or do some logging, or because the class is polymorphic then the class is not the first category, so you don't need to define all 5.
If your class is in the second category and you define at least one of the 5, then you should explicitly =default
the rest of the 5. That is because the rules on when cpy/move ctors/assignments are implicitly declared are a bit complicated. E.g. defining a dtor prevents the implicit declaration of move ctor & assigment
// because I have a custom constructor, I assume I need to also // define copy constructors, move constructors, and a destructor.
Wrong. A custom constructor is not part of the 5.
All of your classes should follow the rule of 0, as neither of them manages resources.
Actually it's pretty rare for a user code to need to implement a rule of 5 class. Usually that's implemented in a library. So, as a user, almost always for to the rule of 0.
The default copy/move ctors/assignments do the expected thing: the copy ones will copy each member, and the move ones will move each member. Well, in the presence of members that aren't movable, or have only some of the 5, or have some of the 5 deleted, then the rules go a little more complex, but there are no unexpected behaviors. The default are good.