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

Я много читал о 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 );,

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

16

Решение

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

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

9

Другие решения

Это похоже на 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 и зачисление компилятора в указатель математики для вас.

4

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

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

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

2
По вопросам рекламы [email protected]