Исключение из правила трех?
Я много читал о С++ Правиле трех. Многие клянутся этим. Но когда правило указано, оно почти всегда включает слово "обычно", "вероятно" или "возможно", указывая на наличие исключений. Я не видел много дискуссий о том, каковы могут быть эти исключительные случаи - случаи, когда правило трех не выполняется, или, по крайней мере, там, где соблюдение этого не дает никаких преимуществ.
Мой вопрос заключается в том, является ли моя ситуация законным исключением из правила трех.. Я считаю, что в описанной ниже ситуации необходимы явно определенный конструктор копирования и оператор назначения копирования, но 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. Вы думаете, и вы решаете. На самом деле кто-то уже пришел к выводу, что правило трех обычно (если вы делаете правильный выбор во время вашего проекта) сводится к правилу два: сделайте ваши ресурсы управляемыми библиотечными объектами (например, вышеупомянутыми умными указателями), и вы обычно можете избавиться от деструктор. Если вам повезет, вы можете избавиться от всех и полагаться на компилятор для генерации кода для вас.
В стороне примечание: ваш конструктор копирования НЕ дублирует созданный компилятор. Вы используете назначение копирования внутри него, в то время как компилятор будет использовать конструкторы копирования. Избавьтесь от назначений в вашем теле конструктора и используйте список инициализаторов. Это будет быстрее и чище.
Хороший вопрос, приятная форма ответа Бена (еще один трюк, чтобы смутить моих коллег на работе), и я рад дать вам обоим варианты.