Programming with Passion

Make the best out of everything.

Saturday 19 March 2016

Declaring the move constructor

Declaring the move constructor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Tool
{
private:
  ResourceA resA_;
  ResourceB resB_;
  // more resources
  
public:
  // Tools's interface
  
  Tool(Tool &&) = default;           // noexcept is deduced
  Tool& operator=(Tool&&) = default; // noexcept is deduced
  
  Tool(Tool const&) = delete;
  Tool& operator=(Tool const&) = delete;
};
static_assert(std::is_move_constructible<Tool>::value, "...");
static_assert(std::is_move_assignable<Tool>::value, "...");
In a way, it is self contradictory. The whole idea behind departing from the Rule of Zero is to separate the interface from the current implementation. Yet, as the comments indicate, the exception specification is deduced from the current implementation, and thus unstable.
Let me be clear about one thing: I am not discussing the usage of the Rule of Zero in the general case. I am only considering its application in a resource-handling non-trivial class, where I want to hide some details, I want to consciously decide on the interface, and I want to clearly communicate the interface to the users.

First things first

Upon the outset of creating my class, I should probably first focus on the interface, and not let my ideas how to implement it alter my interface decisions. I realize this is an idealistic view, probably impractical in some cases, and maybe counterproductive. But it helps us see our class the way the users will see it.
So, after having added a new file to our source base, we start with something like this:
1
2
3
4
5
6
7
8
class Tool
{
private:
  // nothing
  
public:
  // nothing
};
The first (private) ‘nothing’ indicates that we have no private members (either functions or variables). The second (public) ‘nothing’ is a bit deceptive: it may look like we have no public members. But it is not so: we have at least six public member functions in our interface, whether we want them or not. They are all noexcept whether we intend that or not. So just right there, lest we should forget, we have to make a call which of these six we want and which we don’t.
I find destructor the least problematic. We will need it for sure, we will want it noexcept. For all the other special members in order not to accidentally forget to handle them, we had better remove them up front and eventually add them back when we find we need them and the way we want them:
1
2
3
4
5
6
7
8
9
10
11
12
class Tool
{
private:
  // nothing
  
public:
  Tool() = delete;
  Tool(Tool const&) = delete;
  Tool(Tool &&) = delete;
  Tool& operator=(Tool const&) = delete;
  Tool& operator=(Tool &&) = delete;
};
You know, you only need to declare as deleted only a subset of them and the compiler will remove the others, but if you just declare all of them as deleted you do not have to risk that you have remembered the rules incorrectly.
Only now do we start with an empty interface.
We will skip the default constructor in this post. It requires a separate consideration. We will only focus on copying an moving.
Now, I need to answer (at least) three questions:
  1. Will my class be copyable?
  2. Will my class be movable?
  3. Will the move operations be noexcept?
I need to answer these questions right now, before I provide any implementation. Of course, I can (and probably should) think of an initial implementation, consider a range of reasonable implementations, consider what is doable, and what makes sense. But I should not tie my interface to any one possible implementation. Now, that I have no implementation, it should be more clear to me that I cannot let the compiler make the decision for me.
Whether I want my type to be copyable or not depends on the situation. There are standard resource-managing types that are not copyable, like std::fstream and there are standard resource-managing copyable types, like std::vector that clone the resources they manage upon copy. Suppose I decide that I do not want my type to be copyable. If so, I want to declare it clearly; for two reasons:
  1. Users should know my intentions.
  2. Compiler should warn me if I failed to deliver the contract.
Adding copyability atop the contract may seem harmless; but it isn’t (always). For instance, the user may want to be prevented by the compiler if she accidentally triggers an expensive cloning of resources in her program. Or the user may start relying on the copying, and when in the next release of my Tool the copying is suddenly removed, it will break her code.
Removing the copying is simple: we just leave the two =delete declarations.
Now, about moves. I probably want them for a resource managing class. I want them regardless of the implementation I choose. Of course, if my resource-managing subobjects are not movable, I will probably not be able to implement Tool’s move with them, but in that case I want the compiler to tell me about it — not the user. When compiler does warnme, I will be tweaking the implementation until I deliver what I have committed to.
As said in the previous post: the following declaration will not help me:
1
2
3
4
5
class Tool
{
public:
  Tool(Tool &&) = default;
};
This is because ‘default’ here means ‘compiler, do what you would otherwise do by default’. And what compiler sometimes does by default is to not provide the move constructor at all. So, in the previous post, I suggested to put a static_assert to perform the check that is missing, but the result is somewhat ugly: I would not honestly recommend it to anyone. And it has one more serious problem: I am not in control of noexcept: it is still deduced from implementation. I could add yet another static_assert to counteract that, but luckily, there is a simpler way.
First, decide. Do I want my move operations to be noexcept? Typically, unless there are good reasons otherwise, you want a noexcept move for performance reasons (for details,see here). There are some reasons, though, for some types not to offer it. For instance, some implementations of std::list need to have an allocated node even in a moved-from state, and allocating this node during move construction may throw.
So, I’ve made a call, I want my move to be noexcept — not dependent on the implementation. The best way for me to proceed (I have no implementation yet) is to declare the move constructor without providing the definition:
1
2
3
4
5
class Tool
{
public:
  Tool(Tool &&) noexcept;
};
Now, it may look I have completely lost the benefits of compiler-defined functions, and departed from the Rule of Zero, but it is not so. In the ‘cpp’ file (where I typically provide the definitions of my member functions) I can provide a defaulted definition:
1
Tool::Tool(Tool &&) noexcept = default;
So, what is the difference? The difference is in fact huge, and may be really surprising. Explicitly defaulting a member function upon the first declaration has different meaning than defaulting it upon subsequent declarations.
First: as can be seen from the declaration in the class definition (in the header file), the move constructor is there. Always. If, while compiling the cpp file, the compiler cannot provide a definition, it must report an error.
Second: for a similar reason, noexcept is stated by you: it is not deduced. More: when you declare your move constructor noexcept and then in the second declaration you explicitly default it, the compiler will check if the implementation it generated may throw exceptions, and if yes, it will signal the compile-time error. This is one of the rare cases where the compiler statically validates noexcept specifications!
Thus, you are in full control of the declaration: but the compiler provides the definition.
The only downside is that now, although compiler provides the body automatically, the class can never be trivial anymore. But it is never trivial when we are managing a resource anyway.
Note that in order to achieve this effect you do not really have to define the move constructor in a cpp file. If you want to make your move constructor inline, you can define it in the header file, but outside the class definition:
1
2
3
4
5
6
7
class Tool
{
public:
  Tool(Tool &&) noexcept;
};
inline Tool::Tool(Tool &&) noexcept = default;
So, this all leaves us with the following class definition:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Tool
{
private:
  ResourceA resA_;
  ResourceB resB_;
  // more resources
  
public:
  // Tool's interface
  Tool() = delete;
  Tool(Tool const&) = delete;
  Tool& operator=(Tool const&) = delete;
  Tool(Tool &&) noexcept;
  Tool& operator=(Tool &&) noexcept;
};
We could skip the deleted declaration of the default constructor, but I am pretty sure it will confuse some developers which forget the rules what and when is or isn’t implicitly provided by the compiler.
How much is it still in the spirit of the Rule of Zero? I do not know how you interpret it. I still keep one important aspect thereof: the logic for resource handling is separated to another class (ResourceA), the reminder — that is, the business logic — does not deal with resource management at all. This is a special case of the Single Responsibility principle applied to managing resources. But it also follows another important guideline: the interface first.

No comments:

Post a Comment