Я много читал о C ++ Правило трех. Многие клянутся этим. Но когда правило сформулировано, оно почти всегда включает слово «обычно», «вероятно» или «вероятно», указывающее на наличие исключений. Я не видел много дискуссий о том, какими могут быть эти исключительные случаи — случаи, когда правило трех не выполняется или, по крайней мере, соблюдение его не дает никаких преимуществ.
Мой вопрос заключается в том, является ли моя ситуация законным исключением из правила трех. Я считаю, что в ситуации, которую я опишу ниже, явно заданный конструктор копирования и оператор присваивания копии необходимы, но деструктор по умолчанию (неявно сгенерированный) будет работать нормально. Вот моя ситуация:
У меня есть два класса, 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);
мы получаем новый объект a2
это идентично a1
, означающий, что a2.b
идентично a1.b
, означающий, что a2.b.ap
все еще указывает на a1
! Это не то, что мы хотим. Мы должны определить конструктор копирования для A, который дублирует функциональность конструктора копирования по умолчанию, а затем устанавливает новый A::b.ap
чтобы указать на новый объект. Мы добавляем этот код в 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 );
,
Но нет необходимости в явном деструкторе; Поэтому данная ситуация является исключением из правила трех. Я прав?
Не волнуйтесь так сильно о «правиле трех». Правила не должны соблюдаться вслепую; они там, чтобы заставить вас думать. Ты думал И вы пришли к выводу, что деструктор не сделает этого. Так что не пиши. Правило существует так, что вы не забывать написать деструктор, утечка ресурсов.
Тем не менее, этот дизайн создает вероятность того, что B :: ap будет ошибочным. Это целый класс потенциальных ошибок, которые можно было бы устранить, если бы они были единым классом или были связаны более надежным способом.
Это похоже на 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
«s this
указатель с помощью offsetof
макро. Но это сложнее, чем использование static_cast
и зачисление компилятора в указатель математики для вас.
Я иду с @dspeyer. Вы думаете, и вы решаете. На самом деле кто-то уже пришел к выводу, что правило трех обычно (если вы делаете правильный выбор во время разработки) сводится к правилу двух: управляйте своими ресурсами с помощью библиотечных объектов (как вышеупомянутые умные указатели), и вы обычно можете избавиться от них. деструктор. Если вам повезет, вы можете избавиться от всего и положиться на компилятор, который сгенерирует код для вас.
Обратите внимание: ваш конструктор копирования НЕ дублирует созданный компилятором один. Вы используете назначение копирования внутри него, в то время как компилятор будет использовать конструкторы копирования. Избавьтесь от назначений в вашем теле конструктора и используйте список инициализатора. Это будет быстрее и чище.
Хороший вопрос, хороший ответ от Бена (еще одна хитрость, чтобы сбить с толку моих коллег на работе), и я рад дать вам обоим ответ.