Exception to the Rule of Three?

╄→гoц情女王★ 提交于 2019-12-30 02:48:12

问题


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?


回答1:


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.




回答2:


It seems like B is strongly coupled to A, and always should use the A instance that contains it? And that A always contains a B instance? And they access each other's private members via friendship.

One therefore wonders why they are separate classes at all.

But assuming you need two classes for some other reason, here is a straightforward fix that gets rid of all your constructor/destructor confusion:

class A;
class B
{
     A* findMyA(); // replaces B::ap
};

class A : /* private */ B
{
    friend class B;
};

A* B::findMyA() { return static_cast<A*>(this); }

You could still use containment, and find the instance of A from B's this pointer using the offsetof macro. But that's messier than using static_cast and enlisting the compiler to the the pointer math for you.




回答3:


I go with @dspeyer. You think and you decide. Actually someone has already concluded that the rule of three usually (if you make right choices during your design) goes down to the rule of two: make your resources managed by library objects (like above mentioned smart pointers) and you usually can get rid of destructor. If you are lucky enough you can get rid of all and rely on compiler to generate the code for you.

On the side note: your copy constructor DOES NOT duplicate compiler generated one. You use copy assignment inside of it while compiler would use copy constructors. Get rid of assignments in your constructor body and use initializer list. It will be faster and cleaner.

Nice question, nice answer form Ben (another trick to confuse my colleagues at work) and I am happy to give both of you upvotes.



来源:https://stackoverflow.com/questions/15557406/exception-to-the-rule-of-three

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!