41

The rule of 5 states that if a class has a user-declared destructor, copy constructor, copy assignment constructor, move constructor, or move assignment constructor, then it must have the other 4.

But today it dawned on me: when do you ever need a user-defined destructor, copy constructor, copy assignment constructor, move constructor, or move assignment constructor?

In my understanding, implicit constructors / destructors work just fine for aggregate data structures. However, classes which manage a resource need user-defined constructors / destructors.

However, can't all resource managing classes be converted into an aggregate data structure using a smart pointer?

Example:

// RAII Class which allocates memory on the heap.
class ResourceManager {
    Resource* resource;
    ResourceManager() {resource = new Resource;}
    // In this class you need all the destructors/ copy ctor/ move ctor etc...
    // I haven't written them as they are trivial to implement
};

vs

class ResourceManager {
    std::unique_ptr<Resource> resource;
};

Now example 2 behaves exactly the same as example 1, but all the implicit constructors work.

Of course, you can't copy ResourceManager, but if you want a different behavior, you can use a different smart pointer.

The point is that you don't need user-defined constructors when smart pointers already have those so implicit constructors work.

The only reason I would see to have user-defined constructors would be when:

  1. you can't use smart pointers in some low-level code (I highly doubt this is ever the case).

  2. you are implementing the smart pointers themselves.

However, in normal code I don't see any reason to use user-defined constructors.

Am I missing something here?

smci
  • 32,567
  • 20
  • 113
  • 146
SomeProgrammer
  • 1,134
  • 1
  • 6
  • 12
  • If each instance of the class manages its own instance of the resource, and there is also a need to copy (or move) that resource between instances of the class, then the rule of five applies. If your class delegates that copying/moving to an appropriate smart pointer (which, in turn, must handle copying/moving of the resource, and therefore comply with the rule of five) then your class can comply with the rule of zero. – Peter Dec 26 '20 at 10:33
  • 3
    @Peter That is my point. Why can't you always delegate moving / copying to a smart pointer? – SomeProgrammer Dec 26 '20 at 10:35
  • 2
    What if you're writing your own smart pointer? – HolyBlackCat Dec 26 '20 at 10:40
  • 10
    This is called "the rule of zero". – Galik Dec 26 '20 at 10:41
  • @Galik So the rule of 5 is outdated? You should follow the rule of zero? – SomeProgrammer Dec 26 '20 at 10:42
  • @Cyrus Not outdated but the rule of zero is preferred. The rule of 3/5 still apply when you have a member(s) that doesn't manage its own resources. – Galik Dec 26 '20 at 10:43
  • @Galik I fail to understand when you would have a member which does not manage its own resources. Normally , you would have a smart pointer taking care of that. Could you give me example of this please (apart from the case where you are implementing a smart pointer) ? – SomeProgrammer Dec 26 '20 at 10:45
  • 1
    Again, assume you're writing your own smart pointer from scratch. – HolyBlackCat Dec 26 '20 at 10:46
  • 1
    You may have a raw operating system level file handle to manage (for example). – Galik Dec 26 '20 at 10:46
  • What about the implementation of almost all containers? If you implement a vector, you need to allocate (and thus deallocate) memory. Here, the rule of 3/5 is crucial. Keep in mind that not everyone uses STL containers. If you can express your program exclusively by composing other data structures, you do not have to implement any of the five, and the rule of 0 applies. – mrks Dec 26 '20 at 10:47
  • 4
    Anything that has unusual acquire/release semantics. – Galik Dec 26 '20 at 10:47
  • 4
    Your example is simply a bit contrived to make the point. But it's not a good one. Say your constructor creates a new table in a database, which the destructor needs to finalize. How would you avoid this with a smart pointer? – Tasos Papastylianou Dec 26 '20 at 10:54
  • 1
    @TasosPapastylianou Cant std::unique_ptr take a custom deleter as a template argument? – SomeProgrammer Dec 26 '20 at 10:56
  • @Cyrus perhaps but then I'd see that as delegating functionality that logically belongs in your class' responsibilities to an external unrelated entity just to avoid the rule. – Tasos Papastylianou Dec 26 '20 at 11:00
  • @ TasosPapastylianou Quite true. I guess some cases need RAII instead of smart pointers... But if I would be using pointers for resource management I would definitely be using smart pointers. – SomeProgrammer Dec 26 '20 at 11:06
  • Custom deleters only go so far. And, to be honest, using a pointer to manage something that is not a pointer is misleading, so I would not recommend it. I very much do make custom deleters as you suggest when it makes sense. But for other things I write a dedicated resource manager (using the rule or 3/5) so that in the future, I can use that resource manager in other classes that follow the rule of zero. – Galik Dec 26 '20 at 11:07
  • @cyrus - You're assuming, incorrectly, that an existing smart point type is suitable for managing every resource you might need to manage. The smart pointers in the C++ standard library manage particular types of resource, but not others. If you have a need to manage some *other* resource (i.e. the C++ standard library does not provide a suitable class to manage it) then you will be writing your own manager. There are numerous examples of resources that are not supported by the C++ standard library (e.g. resources that are specific to a particular OS). – Peter Dec 26 '20 at 11:42
  • BTW, Notice that converted code (aggregate version) initializes `resource` to `nullptr`, and not to allocated memory. – Jarod42 Dec 26 '20 at 13:29
  • "In normal code" could be seen as a little... un-PC. I think defining "normal code" is a doctorate in and of itself (quantum computing takes a bow) – GrayedFox Dec 30 '20 at 10:56

5 Answers5

71

The full name of the rule is the rule of 3/5/0.

It doesn't say "always provide all five". It says that you have to either provide the three, the five, or none of them.

Indeed, more often than not the smartest move is to not provide any of the five. But you can't do that if you're writing your own container, smart pointer, or a RAII wrapper around some resource.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • 2
    Event this version of the rule isn't one that should *always* be followed. There are exceptions. – eerorika Dec 26 '20 at 11:09
  • @eerorika Curious, what are the exceptions? I don't think I've seen any. – HolyBlackCat Dec 26 '20 at 11:19
  • 11
    Let's say you need to have a pointer that points to a member. If you copy the object, you need to update this pointer. Thus, you need a custom (or deleted) copy constructor and assignment operator. You don't need a destructor. – eerorika Dec 26 '20 at 11:58
  • @HolyBlackCat, I've got a class that's a C++ wrapper around a SQLite database connection. It's got a destructor (so the connection is closed when the object is destroyed), but correct functioning requires that the connection object is unique: it's an error to call a copy constructor, assignment operator, or anything else that would create a second object wrapping the same connection. – Mark Dec 28 '20 at 01:39
  • 5
    @Mark This requires `=delete`ing copy operations, which IMO counts as providing them for the purposes of the rule of 3. – HolyBlackCat Dec 28 '20 at 05:21
19

However, in normal code I don't see any reason to use user-defined constructors.

User provided constructor allows also to maintain some invariant, so orthogonal with rule of 5.

As for example a

struct clampInt
{
    int min;
    int max;
    int value;
};

doesn't ensure that min < max. So encapsulate data might provide this guaranty. aggregate doesn't fit for all cases.

when do you ever need a user-defined destructor, copy constructor, copy assignment constructor, move constructor, or move assignment constructor?

Now about rule of 5/3/0.

Indeed rule of 0 should be preferred.

Available smart-pointers (I include container) are for pointers, collections or Lockables. But resources are not necessary pointers (might be handle hidden in an int, internal hidden static variables (XXX_Init()/XXX_Close())), or might requires more advanced treatment (as for database, an auto commit at end of scope or rollback in case of exceptions) so you have to write your own RAII object.

You might also want to write RAII object which doesn't really own resource, as a TimerLogger for example (write elapsed time used by a "scope").

Another moment when you generally have to write destructor is for abstract class, as you need virtual destructor (and possible polymorphic copy is done by a virtual clone).

Cave Johnson
  • 6,499
  • 5
  • 38
  • 57
Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • 1
    Thank you for clarifying that resource management != pointer. It never occurred to me that you could use an int for resource management... Now I see why RAII is needed in this case. – SomeProgrammer Dec 26 '20 at 11:10
12

The full rule is, as noted, the Rule of 0/3/5; implement 0 of them usually, and if you implement any, implement 3 or 5 of them.

You have to implement the copy/move and destruction operations in a few cases.

  1. Self reference. Sometimes parts of an object refer to other parts of the object. When you copy them, they'll naively refer to the other object you copied from.

  2. Smart pointers. There are reasons to implement more smart pointers.

  3. More generally than smart pointers, resource owning types, like vectors or optional or variants. All of these are vocabulary types that let their users not care about them.

  4. More general than 1, objects whose identity matters. Objects which have external registration, for example, have to reregister the new copy with the register store, and when destroyed have to deregister themselves.

  5. Cases where you have to be careful or fancy due to concurrency. As an example, if you have a mutex_guarded<T> template and you want them to be copyable, default copy doesn't work as the wrapper has a mutex, and mutexes cannot be copied. In other cases, you might need to guarantee the order of some operations, do compare and sets, or even track or record the "native thread" of the object to detect when it has crossed thread boundaries.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
7

Having good encapsulated concepts that already follow the rule of five ensures indeed that you have to worry less about it. That said if you find yourselves in a situation where you have to write some custom logic, it still holds. Some things that come to mind:

  • Your own smart pointer types
  • Observers that have to unregister
  • Wrappers for C-libraries

Next to that, I find that once you have enough composition, it's no longer clear what the behavior of a class will be. Are assignment operators available? Can we copy construct the class? Therefore enforcing the rule of five, even with = default in it, in combination with -Wdefaulted-function-deleted as error helps in understanding the code.

To look closer at your examples:

// RAII Class which allocates memory on the heap.
class ResourceManager {
    Resource* resource;
    ResourceManager() {resource = new Resource;}
    // In this class you need all the destructors/ copy ctor/ move ctor etc...
    // I haven't written them as they are trivial to implement
};

This code could indeed nicely be converted to:

class ResourceManager {
    std::unique_ptr<Resource> resource;
};

However, now imagine:

class ResourceManager {
    ResourcePool &pool;
    Resource *resource;

    ResourceManager(ResourcePool &pool) : pool{pool}, resource{pool.createResource()} {}
    ~ResourceManager() { pool.destroyResource(resource);
};

Again, this could be done with a unique_ptr if you give it a custom destructor. Though, if your class now stores a lot of resources, are you willing to pay the extra cost in memory?

What if you first need to take a lock before you can return the resource to the pool to be recycled? Will you take this lock only once and return all resources or 1000 times when you return them 1-by-1?

I think your reasoning is correct, having good smart pointer types makes the rule of 5 less relevant. However, as indicated in this answer, there are always cases to be discovered where you'll need it. So calling it out-dated might be a bit too far, it's a bit like knowing how to iterate using for (auto it = v.begin(); it != v.end(); ++it) instead of for (auto e : v). You no longer use the first variant, up to the point, you need to call 'erase' where this suddenly does become relevant again.

JVApen
  • 11,008
  • 5
  • 31
  • 67
7

The rule is often misunderstood because it is often found oversimplified.

The simplified version goes like this: if you need to write at least one of (3/5) special methods then you need to write all of the (3/5).

The actual, useful rule: A class that is responsible with manual ownership of a resource should: deal exclusively with managing the ownership/lifetime of the resource; in order to do this correctly it must implement all 3/5 special members. Else (if your class doesn't have manual ownership of a resource) you must leave all special members implicit or defaulted (rule of zero).

The simplified versions uses this rhetoric: if you find yourself in need to write one of the (3/5) then most likely your class manually manages the ownership of a resource so you need to implement all (3/5).

Example 1: if your class manages the acquisition/release of a system resource then it must implement all 3/5.

Example 2: if your class manages the lifetime of a memory region then it must implement all 3/5.

Example 3: in your destructor you do some logging. The reason you write a destructor is not to manage a resource you own so you don't need to write the other special members.

In conclusion: in user code you should follow the rule of zero: don't manual manage resources. Use RAII wrappers that already implement this for you (like smart pointers, standard containers, std::string, etc.)

However if you find yourself in need to manually manage a resource then write a RAII class that is responsible exclusively with the resource lifetime management. This class should implement all (3/5) special members.

A good read on this: https://en.cppreference.com/w/cpp/language/rule_of_three

bolov
  • 72,283
  • 15
  • 145
  • 224