Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Exception to the Rule of Three?

I've read a lot about the C++ Rule of Three. Many people swear by it. But when the rule is stated, it almost always includes a word like "usually," "likely," or "probably," indicating that there are exceptions. I haven't seen much discussion of what these exceptional cases might be -- cases where the Rule of Three does not hold, or at least where adhering to it doesn't offer any advantage.

My question is whether my situation is a legitimate exception to the Rule of Three. I believe that in the situation I describe below, an explicitly defined copy constructor and copy assignment operator are necessary, but the default (implicitly generated) destructor will work fine. Here is my situation:

I have two classes, A and B. The one in question here is A. B is a friend of A. A contains a B object. B contains an A pointer which is intended to point to the A object that owns the B object. B uses this pointer to manipulate private members of the A object. B is never instantiated except in the A constructor. Like this:

// A.h

#include "B.h"

class A
{
private:
    B b;
    int x;
public:
    friend class B;
    A( int i = 0 )
    : b( this ) {
        x = i;
    };
};

and...

// B.h

#ifndef B_H // preprocessor escape to avoid infinite #include loop
#define B_H

class A; // forward declaration

class B
{
private:
    A * ap;
    int y;
public:
    B( A * a_ptr = 0 ) {
        ap = a_ptr;
        y = 1;
    };
    void init( A * a_ptr ) {
        ap = a_ptr;
    };
    void f();
    // this method has to be defined below
    // because members of A can't be accessed here
};

#include "A.h"

void B::f() {
    ap->x += y;
    y++;
}

#endif

Why would I set up my classes like that? I promise, I have good reasons. These classes actually do way more than what I've included here.

So the rest is easy, right? No resource management, no Big Three, no problem. Wrong! The default (implicit) copy constructor for A will not suffice. If we do this:

A a1;
A a2(a1);

we get a new A object a2 that is identical to a1, meaning that a2.b is identical to a1.b, meaning that a2.b.ap is still pointing to a1! This is not what we want. We must define a copy constructor for A that duplicates the functionality of the default copy constructor and then sets the new A::b.ap to point to the new A object. We add this code to class A:

public:
    A( const A & other )
    {
        // first we duplicate the functionality of a default copy constructor
        x = other.x;
        b = other.b;
        // b.y has been copied over correctly
        // b.ap has been copied over and therefore points to 'other'
        b.init( this ); // this extra step is necessary
    };

A copy assignment operator is necessary for the same reason and would be implemented using the same process of duplicating the functionality of the default copy assignment operator and then calling b.init( this );.

But there is no need for an explicit destructor; ergo this situation is an exception to the Rule of Three. Am I right?

like image 688
Sam Kauffman Avatar asked Mar 21 '13 20:03

Sam Kauffman


1 Answers

Don't worry so much about the "Rule of Three". Rules aren't there to be obeyed blindly; they're there to make you think. You've thought. And you've concluded that the destructor wouldn't do it. So don't write one. The rule exists so that you don't forget to write the destructor, leaking resources.

All the same, this design creates a potential for B::ap to be wrong. That's an entire class of potential bugs that could be eliminated if these were a single class, or were tied together in some more robust way.

like image 94
dspeyer Avatar answered Oct 11 '22 11:10

dspeyer