Исключение из правила трех?

Я много читал о С++ Правиле трех. Многие клянутся этим. Но когда правило указано, оно почти всегда включает слово "обычно", "вероятно" или "возможно", указывая на наличие исключений. Я не видел много дискуссий о том, каковы могут быть эти исключительные случаи - случаи, когда правило трех не выполняется, или, по крайней мере, там, где соблюдение этого не дает никаких преимуществ.

Мой вопрос заключается в том, является ли моя ситуация законным исключением из правила трех.. Я считаю, что в описанной ниже ситуации необходимы явно определенный конструктор копирования и оператор назначения копирования, но default (неявно сгенерированный) деструктор будет работать нормально. Вот моя ситуация:

У меня есть два класса: A и B. Здесь речь идет о A. B является другом A. A содержит объект B. B содержит указатель A, который предназначен для указания на объект A, которому принадлежит объект B. B использует этот указатель для управления частными членами объекта A. B никогда не создается, кроме как в конструкторе A. Вот так:

// A.h

#include "B.h"

class A
{
private:
    B b;
    int x;
public:
    friend class B;
    A( int i = 0 )
    : b( this ) {
        x = i;
    };
};

и...

// B.h

#ifndef B_H // preprocessor escape to avoid infinite #include loop
#define B_H

class A; // forward declaration

class B
{
private:
    A * ap;
    int y;
public:
    B( A * a_ptr = 0 ) {
        ap = a_ptr;
        y = 1;
    };
    void init( A * a_ptr ) {
        ap = a_ptr;
    };
    void f();
    // this method has to be defined below
    // because members of A can't be accessed here
};

#include "A.h"

void B::f() {
    ap->x += y;
    y++;
}

#endif

Зачем мне создавать мои классы? Обещаю, у меня есть веские причины. Эти классы действительно делают больше, чем то, что я здесь включил.

Так что все остальное легко, не так ли? Нет управления ресурсами, нет Большой тройки, без проблем. Неправильно! Конструктор копии по умолчанию (неявный) для A не будет достаточным. Если мы это сделаем:

A a1;
A a2(a1);

мы получаем новый объект A a2, который идентичен a1, что означает, что a2.b идентичен a1.b, а это означает, что a2.b.ap все еще указывает на a1! Это не то, что мы хотим. Мы должны определить конструктор копирования для A, который дублирует функциональность конструктора копии по умолчанию, а затем устанавливает новый A::b.ap для указания на новый объект A. Мы добавляем этот код к class A:

public:
    A( const A & other )
    {
        // first we duplicate the functionality of a default copy constructor
        x = other.x;
        b = other.b;
        // b.y has been copied over correctly
        // b.ap has been copied over and therefore points to 'other'
        b.init( this ); // this extra step is necessary
    };

Оператор присваивания копии необходим по той же причине и будет реализован с использованием того же процесса дублирования функциональных возможностей оператора присваивания копии по умолчанию, а затем вызова b.init( this );.

Но нет необходимости в явном деструкторе; Эрго эта ситуация является исключением из правила трех. Я прав?

Ответы

Ответ 1

Не беспокойтесь о "Правиле трех". Правила не должны соблюдаться слепо; они там, чтобы вы подумали. Вы подумали. И вы пришли к выводу, что деструктор этого не сделает. Так что не пишите. Правило существует так, что вы не забудете написать деструктор, утечка ресурсов.

Тем не менее, эта конструкция создает вероятность того, что B:: ap окажется неправильным. Это целый класс потенциальных ошибок, которые можно было бы устранить, если бы они были одним классом или были связаны друг с другом более надежным способом.

Ответ 2

Кажется, что B сильно связан с A и всегда должен использовать экземпляр A, который содержит его? И что A всегда содержит экземпляр B? И они получают доступ к другим частным членам через дружбу.

Поэтому возникает вопрос, почему они вообще являются отдельными классами.

Но предполагая, что вам нужны два класса по какой-то другой причине, вот простое исправление, которое избавляет вас от путаницы вашего конструктора/деструктора:

class A;
class B
{
     A* findMyA(); // replaces B::ap
};

class A : /* private */ B
{
    friend class B;
};

A* B::findMyA() { return static_cast<A*>(this); }

Вы все равно можете использовать сдерживание и найти экземпляр A из B this указателя, используя макрос offsetof. Но это беспорядочно, чем использование static_cast и привлечение компилятора к математике указателя для вас.

Ответ 3

Я иду с @dspeyer. Вы думаете, и вы решаете. На самом деле кто-то уже пришел к выводу, что правило трех обычно (если вы делаете правильный выбор во время вашего проекта) сводится к правилу два: сделайте ваши ресурсы управляемыми библиотечными объектами (например, вышеупомянутыми умными указателями), и вы обычно можете избавиться от деструктор. Если вам повезет, вы можете избавиться от всех и полагаться на компилятор для генерации кода для вас.

В стороне примечание: ваш конструктор копирования НЕ дублирует созданный компилятор. Вы используете назначение копирования внутри него, в то время как компилятор будет использовать конструкторы копирования. Избавьтесь от назначений в вашем теле конструктора и используйте список инициализаторов. Это будет быстрее и чище.

Хороший вопрос, приятная форма ответа Бена (еще один трюк, чтобы смутить моих коллег на работе), и я рад дать вам обоим варианты.