Update. I have updated the post a bit, as it misled a number of people to think that you need to define the move constructor in a cpp
file. This is not so. I have now also highlighted another important feature of my solution: statically checked noexcept
specification.
I am not satisfied with the solution I gave in the previous post. The proposed interface was this:
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:
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.
One of them is the destructor. I find it 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:
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:
- Will my class be copyable?
- Will my class be movable?
- 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:
- Users should know my intentions.
- 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 warn me, 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:
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:
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:
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:
class Tool { public: Tool(Tool &&) noexcept; }; inline Tool::Tool(Tool &&) noexcept = default;
So, this all leaves us with the following class definition:
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.
Acknowledgements
I am grateful to David Krauss for pointing out to me the difference between defaulting functions upon the first an subsequent declarations. And to Tomasz Kamiński for suggesting improvements to the post.
Pingback: Links of the Week, W38 2015 | One More Game-Dev and Programming Blog
What about the following in-class definition of the move constructor?
class Tool {
…
Tool(Tool &&) noexcept = default;
…
};
I tried this code with clang 3.6 and it provides the exact same AST whether there is a noexcept specifier or not. It is quite misleading (for me) that in case of in-class declarations, the noexcept specifier is not taken into account if we do provide the `=default`.
Did you try it with
noexcept(false)
?If I try this example:
Clang 3.6 reports an error: “exception specification of explicitly defaulted move constructor does not match the calculated one”. This is in accordance with the Standard (see 8.4.2/2 and 15.4/3).
In contrast, GCC accepts the code, but
Tool
isnoexcept(false)
. I guess it is a bug in GCC.This is an addition to my previous comment.
The below code compiles (clang 3.6) with an error.
class A {
A(A&&) = delete;
};
class Tool
{
private:
A resA_;
public:
Tool(Tool &&) noexcept = default;
Tool(Tool const&) = delete;
};
And the error is: exception specification of explicitly defaulted move constructor does not match the calculated one
Tool(Tool &&) noexcept = default;
^
I.e., if the resource class is non-movable we get the desired error. But here we add `=default` at the first declaration. So, do we need to define out-of-class the move constructor?
Try it this way:
Now, it compiles fine, but I would like a compile-time error instead.
Okay, you convinced me. Also, the clang compiler gives a clever error message, when we do an out-of-class defaulting: “move constructor of ‘Tool’ is implicitly deleted because field ‘resA_’ has a deleted move constructor”.
For me this means, it should be a kind of best practice to do the defaulting of compiler generated functions (copy and move ops, and dtor as well). And do the defaulting out-of-class if we consider copy and move part of the public interface (and we do). Am I having the right conclusion?
These two posts of mine are only relevant in cases where we have private (internal) data members. In the cases where everything is public:
You can just go with the defaults.
Don’t you lose the inline chance, if you provide the defaulted definition (Tool::Tool(Tool &&) noexcept = default) in the cpp file?
You do not really need to put the definition in a CPP file. You just need to put the constructor definition outside the class definition:
You can put this in a header file. You still get the benefits I described, and have an inline function.
Now the question is why do you need this function to be inline? Is it performance? In that case the effect might be quite opposite to your expectations (see here). Or do you want to provide a header-only library? In that case inline functions is the only way to go, even at the expense of run-time performance.
What do you think about such a case,
where there is a resource managing type X
with well defined copying
while X is non-moveable (or moving is equivalent to copying)?
X shall provide copy ctor/assignment.
In case of move ctor/assignment there are three options:
1) do not provide anything (like c++03)
2) delegate work to copying
3) explicitly delete moving
Option 1) violates Rules of 0/5/6.
Option 2) misleads user (and type_traits)
Option 3) expresses intentions, but…
When move is explicitly deleted it still participates in overload resolution.
That makes it impossible to use X i.e. as return value in 3), while 1) and 2) works well.
http://melpon.org/wandbox/permlink/csGkxzNnUm3JBXRn
That’s a good question. First, I believe that when a type manages a resource and provides a copy constructor (that supposedly clones the resource it should be possible to offer a ‘stealing’ move constructor.
But the issue still remains. I do not have a ready answer for that. Option 3, as you point out, is not an option. Options 1 and 2 appear satisfactory.
You say that option 1 violates the rule of something. I would say that our primary goal is to write correct and robust programs; following any principles is secondary, and cannot compromise the primary goal. Option 1 does not have problems I have described for move operations: move constructor is silently removed, but it is only the consequence of other public declarations: not the private ones.
I also do not follow how option 2 misleads
type_traits
. The type is movable, as it should be. Copyability is a superset of movability. Not sure how it misleads the user. Although, when someone sees a declaration like this:They might think it is a recursive call. But I think it just requires getting used to. I think I would go with option 1: I wouldn’t like to repeat the constexpr and noexcept qualifiers.
really nice post
Looks like a typo, when you refer to “destructor” it should read “constructor”
Thanks. Word “destructor” appears only in one place, and I did mean destructor there. I reworded the sentence a bit in order to avoid confusion. I hope it is better now.