依赖类作为其他类成员

10

我有一个类B,需要构造一个类A的实例:

class B
{
    B(A* a); // there is no default constructor
};

现在我想创建一个包含B成员的类,因此我还需要将A作为成员添加并提供给B的构造函数:

class C
{
    C() : a(), b(&a) {}
    A a; // 1. initialized as a()
    B b; // 2. initialized as b(&a) - OK
};

但问题在于,如果有人偶尔更改类中变量定义的顺序,它将会出错。

class C
{
    C() : a(), b(&a) {}
    B b; // 1. initialized as b(&a) while "a" uninitialized
    A a; // too late...
};

有没有不修改 AB 类的好方法来解决这个问题?谢谢。


3
这就是为什么你应该始终编译时打开所有警告。如果被告知,所有编译器都会在这里发出警告。 - Alexandre C.
你可以简单地将其保留原样,并添加一个巨大的警告作为注释。此外,考虑启用所有警告,通常编译器会提供警告,如果字段声明顺序(这是重要的)与初始化列表的顺序不同;如果有人偶然交换了两个字段的顺序,你会得到这样的警告。 - Matteo Italia
@Alexandre C.,@Matteo Italia:我已经在MSVC和gcc(mingw 3.4.5)中尝试过了 - 默认情况下没有警告... - Roman L
顺便提一下,如果你在使用-Wall编译时,g++可以为这种情况提供警告(我找不到触发此警告的具体警告)。 - Tim Martin
g++ 4.4.5使用-Wall选项会产生这样的警告。 - Matteo Italia
显示剩余2条评论
6个回答

7

有没有一种不修改类A和B的好方法来解决这个问题?

打开编译器警告;对于gcc,使用-Wreorder(包含在-Wall中):

cc1plus:正在处理警告
t.cpp:在构造函数'A::A()'中:
第3行:警告:'A :: y'将在之后初始化
第3行:警告:   'int A :: x'
第2行:警告:   在此初始化时

或者,使用类似lint的工具来检测这个问题。


但问题在于,如果有人偶尔更改了类中变量定义的顺序...为什么会这样做呢?我怀疑你对可能发生的事情过于担心了。即便如此,在类中留下注释也是可以的:
A a;  // Must be listed before member 'b'!
B b;

不要低估适当放置的注释的力量。 :) 然后允许有意忽略它们的人得到他们应得的结果;毕竟你正在使用C++。


我并不像你说的那样担心,我只是好奇而已。而且我也不能在全世界的机器上都开启编译器警告 :) - Roman L
@7vies:如果这是为了你的项目,你应该对使用的编译器(和lint)选项有所发言权。除此之外,我已经学会了即使我有时间也不能修复其他人的问题。 :) - Fred Nurk

5

请使用广为人知的C++习惯用法从成员继承来解决这个问题。

定义一个基类如下:

class C_Base
{
    A a; //moved `A a` to the base class!
    C_Base() : a() {}
};

class C : public C_Base
{
    C() : b(&a) {}
    B b; // 1. initialized as b(&a) while "a" uninitialized
    //A a; // too late...
};

现在,ab 之前保证被初始化。

创建一个基类只是为了强制初始化顺序,这不是很奇怪吗? - Roman L
@7vies:对我来说似乎是这样。几乎可以说确定性初始化顺序很有用... - James
@7vies:是的,但更奇怪的是想到有人会改变类中变量的顺序。 - Pawel Zubrycki
@Fred:我同意!但是如果有人担心特定成员a必须在b之前初始化,那么这可能很有用。 - Nawaz
@Fred:有用和过度设计不是同一回事。Base-From-Member 是过度设计,但这并不意味着它没有用处。 - Nawaz
显示剩余5条评论

2
将 b 存储在 unique_ptr 中,然后将其设置在函数体中,而非初始化列表中:
class C
{
    C() :a() {
        b = std::unique_ptr<B>(new B(&a));
    }
    A a;
    std::unique_ptr<B> b;
};

0
一种选择是不显式存储A,而是使用动态分配来创建一个新的A并存储在B中:
class C {
public:
       C() : b(new A) {
           // handled in initialization list
       }
private:
       B b;
};

由于这确保了A在B之前创建,因此这应该可以防止此问题的发生。


1
那么谁会“删除A”呢?我的意思是,你使用“new”分配内存,但你不能“delete”它,而且“B”也不会处理这个问题。 - Roman L

0
问题在于第三个例子会让你自己给自己惹麻烦。在C++中,类/结构体中成员变量的顺序很重要。无论你如何解决你的特定问题,如果由于糟糕的类设计/成员布局而向构造函数传递未初始化的数据,你将使用未初始化的数据,并可能得到未定义的行为,这取决于代码的类型。
针对你的特定示例,如果B确实需要一个A,并且关系是一对一的,为什么不创建一个新的类AB,其中包含正确顺序的A对象和B对象,并将A的地址传递给B。也就是说:
class AB
{
public:
  AB():b_(&a_) {}

private:
  A a_;
  B b_;
};

现在类 C 可以通过使用 AB 而不是 AB 来避免排序问题:

class C
{
public:
  ...
private:
  AB ab_;
};

如前所述,这当然假定 AB 之间是一对一的关系。如果一个 A 对象可以被多个 B 对象共享,情况就会更加复杂。

第三个例子只是为了说明第二个例子为什么不好。我知道它不好,我的问题是如何使它正确。 - Roman L

0

我不确定您对 C 类的实现和结构有多少控制权,但是否有必要在类 C 中使用对象本身?您能否重新定义类以使用指针,然后将它们从初始化列表中移动,例如:

class C
{
   C()
   {
     a = new A;
     b = new B(a);
   }
   ~C() {delete a; delete b;}

   A* a;
   B* b;
};

这样做避免了声明顺序的问题,但是会带来新的问题,需要确保它们被正确创建。此外,如果您经常创建很多 C,初始化列表会稍微快一些。


但是,你如何防止任何人重新排列构造函数中的行呢?;-) - Bo Persson
@Bo 放弃编程,去骑自行车吧 :P - spbots
@Bo Persson:对我来说,重新排列代码行(更改代码)和重新排列成员定义(例如在重构期间)之间的区别非常明显,所以我不明白你的观点。 - Roman L

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接