Есть ли способ спроектировать два класса, которые конфиденциально манипулируют данными друг друга? - PullRequest
2 голосов
/ 05 октября 2011

Справочная информация: я пишу интерфейс оболочки C ++ для библиотеки C, написанной не мной. Мои классы-оболочки имитируют структуры библиотеки, и в этой библиотеке некоторые члены struct b указывают на члены struct a. Документация к этой библиотеке гласит: «Не уничтожайте переменную struct a перед одной из struct b». На самом деле есть ситуации, когда это должно быть разрешено, поэтому я хочу лучше справиться с ситуацией в коде. Поэтому в моей оболочке, если экземпляр class A с одним или несколькими экземплярами class B, указывающими на него, будет уничтожен до всех этих экземпляров B, я хочу скопировать данные из A в каждый экземпляр B. Я работаю с открытыми функциями-членами, как таковые:

// 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); }
};

Как видно из вышесказанного, функции-члены register и copy являются только и должны вызываться только из ctor / dtors. Другими словами, пользователи моих классов не должны никогда вызывать эти функции. Следовательно, в соответствии с принципами инкапсуляции и философией Скотта Майера «сделать интерфейсы простыми в использовании, правильными и трудными в неправильном использовании», я хочу поместить эти функции в частные разделы A и B. Однако это, очевидно, означает, что я больше не мог вызывать их из класса сверстников. Я рассмотрел использование friend функций следующим образом:

// 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; }
};

Однако в этом коде есть как минимум три неправильные вещи: 1) существуют циклические отношения, которые не могут быть разрешены, 2) каждый класс все еще пытается получить доступ к закрытым членам другого класса в объявлениях friend и 3) он не очень хорошо инкапсулирован или интуитивно понятен.

Поэтому я снова спрашиваю: есть ли лучший способ спроектировать два класса, которые конфиденциально манипулируют данными друг друга?

Ответы [ 4 ]

7 голосов
/ 05 октября 2011

Да, посмотрите на друзей C ++.

class B;

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

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

FAQ C ++ содержит приятное объяснение дружбы .

2 голосов
/ 05 октября 2011

Еще один приятный вариант, который мне действительно нравится рекламировать, - это идиома passkey .

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()); /*...*/ }
};

Важные функции являются общедоступными, но только соответствующие классы будут когда-либо иметь к ним доступ, потому что никто другой не может создать необходимый «ключ», благодаря частным ctors и дружбе. Это также очень мелкозернистый контроль доступа, так как он не просто предоставляет доступ через доску, как обычная дружба.

Надеюсь, концепция этого понятна, если нет, прочитайте ссылку. Это также может представлять интерес.

0 голосов
/ 05 октября 2011

Чтобы избежать циклических ссылок, сделайте две вещи:

Во-первых, объявляйте классы друзьями друг друга, а не пытайтесь объявлять методы друзей. Это означает, что вам нужно быть более осторожным, чем перекладывать все проверки на ваш компилятор.

Во-вторых, не определяйте ваши функции встроенными, вместо этого, как это (много сокращений упущений):

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.
{
}
0 голосов
/ 05 октября 2011

Считаете ли вы, что у B есть shared_ptr к соответствующему A?Затем, когда исходный A shared_ptr уходит, любые B, которые ссылаются на него, сохраняют объект номинально, пока все ссылающиеся экземпляры B также не исчезнут, и в этот момент он очищен.Это устраняет циклическую ссылку и необходимость взаимодействия с частными данными.

...