2

Background: I'm writing a C++ wrapper interface for a C library not written by me. My wrapper classes mimic the library structs, and in this library, some members of struct b point to members of struct a. The documentation for this library says, "Don't destroy a variable of struct a before one of struct b." There are actually situations where this should be allowed, so I want to handle the situation better in code. Therefore, in my wrapper, should an instance of class A with one or more instances of class B pointing to it be destroyed before all of those instances of B, I want to copy the data from A to every instance of B. Currently, I handle this with public member functions, as such:

// some code shortened or not shown

struct a {
  int d;                            // in reality, data is much more complicated
};
struct b {
  int* d;
};

class B;
class A {
  struct a a_;
  vector<B*> registered_bs_;                // should probably use unordered_set
public:
  ~A(void) { for (iterator it: registered_bs_) (*it)->copyA(); }    // C++0x for
  void registerB(B* b)   { registered_bs_.push_back(b); }
  void unregisterB(B* b) { registered_bs_.erase(b); }   // find() code not shown
};

class B {
  struct b b_;
  A* pa_;
public:
  B(A& a): b_(), pa_(0) { a.registerB(this); pa_ = &a; }
  ~B(void) { pa_->unregisterB(this); if (b_.d) delete b_.d; } // if B goes first
  void copyA(void) { b_.d = new int(*b_.d); }
};

As can be seen from the above, the register and copy member functions are only and should only be called from ctor/dtors. In other words, users of my classes should never call these functions. Therefore, in keeping with the principles of encapsulation and Scott Meyer's philosophy of "make interfaces easy to use correctly and difficult to use incorrectly," I want to put those functions in the private sections of A and B. However, this obviously means that I could no longer invoke them from their peer class. I've considered using friend functions, as follows:

// this doesn't work

class B;
class A {
  struct a a_;
  vector<B*> registered_bs_;

  void copyA(B& b) { b.b_.d = new int(*(b.b_.d)); }                 // circular
  friend void B::registerB(A& a);                                   // circular
  friend void B::unregisterB(A& a);                                 // circular
public:
  ~A(void) { for (iterator it: registered_bs_) copyA(*it); }       // C++0x for
};

class B {
  struct b b_;
  A* pa_;

  void registerB(A& a)   { a.registered_bs_.push_back(this); }
  void unregisterB(A& a) { a.registered_bs_.erase(this); }  // find() not shown
  friend void A::CopyA(B& b);
public:
  B(A& a): b_(), pa_(0) { registerB(a); pa_ = &a; }
  ~B(void) { unregisterB(*pa_); if (b_.d) delete b_.d; }
};

However, there are at least three things wrong with this code: 1) there's a circular relationship that can't be resolved, 2) each class is still trying to access private members of the other class in the friend declarations, and 3) it isn't well encapsulated or intuitive.

Therefore, I ask again: is there a better way to design two classes that privately manipulate each other's data?

LowTechGeek
  • 431
  • 3
  • 12
  • It sounds like `class A` should really be multiple composed classes, and allow some of the resources to live after it dies. You could introduce a `class C` as encapsulation of access to the shared `A` data, and copies/captures the resources when (before) its `A` is deallocated. The Observer Pattern might help you with this (triggering the copy). If you want to keep around only one copy of the extra `A` data, you could have `A` copy it before it dies, and pass smart pointers of this data to its observers. – Merlyn Morgan-Graham Oct 05 '11 at 01:19

4 Answers4

7

Yes, look into C++ friends.

class B;

class A
{
    friend class B;
    // ...
};

class B
{
    friend class A;
    // ...
};

The C++ FAQ has a nice explanation of friendship.

André Caron
  • 44,541
  • 12
  • 67
  • 125
  • _Sigh_. I knew about friends. And yet, I didn't. This link clarified it for me. I originally didn't want to use class friendship as I didn't want the peer classes to have access to _all_ private data of the other class. But, considering _I_ am the author of both classes, I agree that this is the proper method. – LowTechGeek Oct 04 '11 at 23:57
  • 2
    +1, when two classes need to be really tightly coupled, it's often better for them to be friends than to expose more functions than necessary in their public scope. – Shautieh Oct 05 '11 at 00:01
  • @ildjarn, when I removed the `class B;` forward declaration and added the `friend class B;` statement in class A, g++ complained that B was not declared when I tried to create a pointer to it in A on the line right after the `friend` statement. Therefore, I had to use the `friend` statement _and_ the forward declaration. – LowTechGeek Oct 06 '11 at 22:39
  • @LowTechGeek : You and André are right, I was wrong; I'm not sure why I had that impression... :-/ – ildjarn Oct 06 '11 at 22:55
2

Another nice option that I really like to advertise is the passkey idiom.

class A;
class B;

class KeyForA{
  KeyForA(){} // private ctor
  friend class B;
};

class KeyForB{
  KeyForB(){} // private ctor
  friend class A;
};

class A{
  // ...
public:
  // ...
  void registerB(B* b, KeyForA){ /*...*/ }
  void unregisterB(B* b, KeyForA){ /*...*/ }

  ~A(){ for(auto it : registered_bs_) (*it)->copyA(KeyForB()); }
};


class B{
  A* pa_
public:
  B(A& a){ a.registerB(this, KeyForA()); pa_ = &a; }
  ~B(){ pa_->unregisterB(this, KeyForA()); /*...*/ }
};

The important functions are public, but only the respective classes will ever have access to them, because no-one else can create the needed "key", thanks to the private ctors and friendship. This also very fine grained access control, as it not just grants access across the board like normal friendship.

I hope the concept behind this is clear, if not, read the link. This might also be of interest.

Community
  • 1
  • 1
Xeo
  • 129,499
  • 52
  • 291
  • 397
0

Did you consider just having B have a shared_ptr to its corresponding A? Then when the original A shared_ptr goes away, any B's that reference it keep the object nominally around until all the referencing B instances also go away at which point it's cleaned up. This removes the circular reference and the need to interact with private data.

Mark B
  • 95,107
  • 10
  • 109
  • 188
  • Yes. However, in the production code, `class A` stores a _lot_ of data, in the order of hundreds of MBs, whereas the amount of data in A that B points to is only about 1% of that. So, if an A is going to be destroyed, I want it to be, but I still want all B's to point to valid data. – LowTechGeek Oct 05 '11 at 00:04
0

To avoid the circular references, do two things:

First, declare the classes friends of each other, rather than trying to declare friend methods. This means you will have to be more careful rather than offloading all the checking to your compiler.

Second, don't define your functions inline, instead like this (a great deal of shortening omissions):

class A
{
  void copyA(B& b);
};

class B
{
};
// B is now fully defined, can refer to members freely.
void A::copyA(B& b) // optional: Use the inline keyword before void.
{
}
01d55
  • 1,872
  • 13
  • 22