【问题标题】:Is there a way to design two classes that privately manipulate each other's data?有没有办法设计两个私下操作彼此数据的类?
【发布时间】:2011-10-04 22:37:58
【问题描述】:

背景:我正在为不是我编写的 C 库编写 C++ 包装器接口。我的包装类模仿库结构,在这个库中,struct b 的一些成员指向struct a 的成员。这个库的文档说,“不要在struct b 之一之前销毁struct a 的变量。”实际上在某些情况下应该允许这样做,所以我想在代码中更好地处理这种情况。因此,在我的包装器中,如果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 中调用。换句话说,我的课程的用户应该永远调用这些函数。因此,本着封装的原则和 Scott Meyer 的“让接口容易正确使用,难于错误使用”的理念,我想把这些函数放在 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) 它没有很好地封装或直观。

因此,我再次问:有没有更好的方法来设计两个私下操作彼此数据的类?

【问题讨论】:

  • 听起来class A 真的应该是多个组合类,并允许一些资源在它死后仍然存在。您可以引入class C 作为对共享A 数据的访问的封装,并在(之前)其A 被释放时复制/捕获资源。观察者模式可能会帮助您解决这个问题(触发副本)。如果您只想保留额外的A 数据的一份副本,您可以让A 在它死之前复制它,并将此数据的智能指针传递给它的观察者。

标签: c++ class-design


【解决方案1】:

是的,看看 C++ 的朋友。

class B;

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

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

C++ 常见问题解答有一个nice explanation of friendship

【讨论】:

  • 叹息。我知道朋友。然而,我没有。这个链接为我澄清了它。我最初不想使用班级友谊,因为我不希望对等班级访问其他班级的所有私人数据。但是,考虑到 是这两个类的作者,我同意这是正确的方法。
  • +1,当两个类需要真正紧密耦合时,最好让它们成为朋友,而不是在公共范围内公开更多不必要的功能。
  • @ildjarn,当我删除 class B; 前向声明并在 A 类中添加 friend class B; 语句时,g++ 抱怨当我尝试在 A 中创建指向它的指针时未声明 B friend 语句之后的行。因此,我不得不使用friend 声明前向声明。
  • @LowTechGeek :你和安德烈是对的,我错了;我不知道为什么我有这种印象...... :-/
【解决方案2】:

我非常喜欢宣传的另一个不错的选择是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()); /*...*/ }
};

重要的功能是公开的,但只有各自的类才能访问它们,因为没有其他人可以创建所需的“密钥”,这要归功于私有 ctors 和友谊。这也是非常精细的访问控制,因为它不仅像普通友谊一样授予全面访问权限。

我希望这背后的概念很清楚,如果没有,请阅读链接。 This 也可能感兴趣。

【讨论】:

  • 我忘记了这个成语。不错的电话。 +1。
【解决方案3】:

您是否考虑过让B 与其对应的A 有一个shared_ptr?然后,当原始的A shared_ptr 消失时,任何引用它的B 都会名义上保留对象,直到所有引用的B 实例也消失,此时它被清除。这消除了循环引用以及与私有数据交互的需要。

【讨论】:

  • 是的。但是,在生产代码中,class A 存储了 很多 数据,大约为数百 MB,而 B 指向的 A 中的数据量仅为其中的 1% 左右。所以,如果一个 A 将被销毁,我希望它被销毁,但我仍然希望所有 B 都指向有效数据。
【解决方案4】:

为了避免循环引用,做两件事:

首先,声明彼此的类朋友,而不是试图声明朋友方法。这意味着您必须更加小心,而不是将所有检查都交给编译器。

其次,不要内联定义你的函数,而是像这样(大量缩短省略):

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.
{
}

【讨论】:

    猜你喜欢
    • 2020-12-01
    • 2016-07-31
    • 2012-07-08
    • 1970-01-01
    • 2015-06-16
    • 2022-11-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多