Зависимые классы как другие члены класса

У меня есть класс 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...
};

Есть ли хороший способ решить эту проблему без изменения классов A и B? Спасибо.

Ответ 1

Есть ли хороший способ разрешить это без изменения классов A и B?

Включите предупреждения компилятора; для gcc это -Wreorder (который включен в -Wall):

cc1plus: warnings being treated as errors
t.cpp: In constructor 'A::A()':
Line 3: warning: 'A::y' will be initialized after
Line 3: warning:   'int A::x'
Line 2: warning:   when initialized here

В качестве альтернативы используйте инструмент, подобный lint-like, который обнаруживает это.


Но проблема в том, что если кто-то время от времени меняет порядок определения переменных в классе...

Зачем им это делать? Я подозреваю, что вы слишком много беспокоитесь о том, что может случиться. Тем не менее, вы можете оставить комментарий в классе:

A a;  // Must be listed before member 'b'!
B b;

Не стоит недооценивать силу хорошо сделанных комментариев.:) Тогда позвольте тому, кто целенаправленно игнорирует их, чтобы получить то, что они заслуживают; вы все-таки используете С++.

Ответ 2

Для решения этой проблемы используйте хорошо известную идиому С++ под названием Base-from-Member.

Определите базовый класс как,

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

Теперь a гарантированно инициализируется до b.

Ответ 3

Храните b в файле unique_ptr и установите его в теле, а не в списке инициализаторов:

class C
{
    C() :a() {
        b = std::unique_ptr<B>(new B(&a));
    }
    A a;
    std::unique_ptr<B> b;
};

Ответ 4

Один вариант заключается в том, чтобы явно не хранить A, а вместо этого использовать динамическое распределение для создания нового A для хранения в B:

class C {
public:
       C() : b(new A) {
           // handled in initialization list
       }
private:
       B b;
};

Так как это гарантирует, что A создается до B, это должно помешать этой проблеме когда-либо возникать.

Ответ 5

Проблема в том, что вы стреляете в ногу третьим примером. В С++ порядок членов переменных в классе/структуре имеет значение. Независимо от того, как вы решаете свою конкретную проблему, если вы передаете неинициализированные данные конструктору из-за плохого дизайна/макета класса, вы будете работать с унифицированными данными и, возможно, получить поведение undefined, в зависимости от типа кода в место.

Чтобы обратиться к вашему конкретному примеру, если B действительно требует A, а отношения - один к одному, почему бы не создать новый класс AB, который имеет как объект A, так и объект B в правильном порядке и передать адрес A на B. То есть:

class AB
{
public:
  AB():b_(&a_) {}

private:
  A a_;
  B b_;
};

теперь класс C может избежать проблемы с упорядочением, используя AB вместо A и B:

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

Как уже упоминалось, это, конечно, предполагает соотношение 1:1 между A и B. Если объект A может использоваться несколькими объектами B, все становится более сложным.

Ответ 6

Я не уверен, сколько у вас контроля над реализацией и структурой C, но нужно ли использовать сами объекты в классе C? Не могли бы вы переопределить класс для использования указателей вместо этого, а затем переместить их из списка инициализации, например.

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

   A* a;
   B* b;
};

Это позволяет избежать проблемы с порядком в объявлении, но дает новую проблему обеспечения правильного создания. Кроме того, если вы очень часто создаете A LOT C, список инициализации выполняется немного быстрее.