Абстрактные базовые классы и конструкция копирования, практические правила

10

Часто бывает полезно иметь абстрактный базовый класс для изоляции интерфейса объекта.

Проблема в том, что конструкция копирования, IMHO, по умолчанию в C ++ в значительной степени нарушена, а конструкторы копирования генерируются по умолчанию.

Итак, что за ошибки, когда у вас есть абстрактный базовый класс и необработанные указатели в производных классах?

class IAbstract
{
    ~IAbstract() = 0;
}

class Derived : public IAbstract
{
    char *theProblem;
    ...
}

IAbstract *a1 = new Derived();
IAbstract a2 = *a1;//???

А теперь вы полностью отключили конструкцию копирования для всей иерархии? Объявить копию конструкции как частную в IAbstract?

Есть ли правила трех с абстрактными базовыми классами?

кодировщик
источник
1
используйте ссылки вместо указателей :)
tp1
@ tp1: или какой-нибудь умный указатель по крайней мере.
Бенджамин Баннье
1
Иногда вам просто нужно работать с существующим кодом ... Вы не можете изменить все в одно мгновение.
Кодер
Как вы думаете, почему конструктор копирования по умолчанию не работает?
BЈовић
2
@Coder: руководство по стилю Google - куча мусора и абсолютно отстой для любой разработки на C ++.
DeadMG

Ответы:

6

Конструкция копирования в абстрактном классе в большинстве случаев должна быть закрытой, так же как и оператор присваивания.

Абстрактные классы по определению считаются полиморфными. Таким образом, вы не знаете, сколько памяти использует ваш экземпляр, и поэтому не можете безопасно скопировать или назначить его. На практике вы рискуете нарезать ломтиками: /programming/274626/what-is-the-slicing-problem-in-c

Полиморфный тип в C ++ нельзя манипулировать значением. Вы управляете ими по ссылке или по указателю (или любому интеллектуальному указателю).

Это причина, по которой Java сделала объект управляемым только по ссылке, и почему в C # и D есть разделение между классами и структурами (первый - полиморфный и ссылочный тип, второй - не полиморфный и тип значения).

deadalnix
источник
2
Вещи в Java не лучше. В Java больно копировать что-либо, даже когда это действительно необходимо, и очень легко забыть это сделать. Таким образом, вы получите неприятные ошибки, когда две структуры данных имеют ссылку на один и тот же класс значений (например, Date), а затем одна из них изменяет Date, а другая структура данных теперь нарушена.
Кевин Клайн
3
Мех. Это не проблема - любой, кто знает C ++, не допустит этой ошибки. Что еще более важно, нет никакой причины манипулировать полиморфными типами по значению, если вы знаете, что делаете. Вы инициируете полную реакцию коленного рефлекса из-за технически узкой возможности. NULL-указатели - большая проблема.
DeadMG
8

Конечно, вы можете просто сделать его защищенным и пустым, чтобы можно было выбирать производные классы. Однако, в более общем смысле, ваш код в любом случае запрещен, потому что его невозможно создать IAbstract- потому что он имеет чисто виртуальную функцию. Таким образом, это, как правило, не проблема - классы вашего интерфейса не могут быть созданы и, следовательно, никогда не могут быть скопированы, а ваши более производные классы могут запретить или продолжить копирование, как они пожелают.

DeadMG
источник
А что если существует иерархия Abstract -> Derived1 -> Derived2, и Derived2 назначен Derived1? Эти темные уголки языка все еще удивляют меня.
Кодер
@ Кодер: Это обычно рассматривается как PEBKAC. Тем не менее, более прямо, вы всегда можете объявить частное operator=(const Derived2&)в Derived1.
DeadMG
Хорошо, возможно это действительно не проблема. Я просто хочу убедиться, что класс безопасен от злоупотреблений.
Кодер
2
И за это вы должны получить медаль.
Мировой инженер
2
@ Кодер: злоупотребление кем? Лучшее, что вы можете сделать, - это легко написать правильный код. Бессмысленно пытаться защищаться от программистов, которые не знают C ++.
Кевин Клайн
2

Делая ctor и присваивание частным (или объявляя их как = delete в C ++ 11), вы отключаете копирование.

Дело в том, ГДЕ вы должны это сделать. Чтобы остаться с вашим кодом, IAbstract не является проблемой. (обратите внимание, что делая то, что вы сделали, вы назначаете *a1 IAbstractподобъект a2, теряя все ссылки на Derived. Присвоение значения не является полиморфным)

Проблема идет с Derived::theproblem. Копирование производного в другое может фактически делиться *theproblemданными, которые могут быть не предназначены для совместного использования (есть два экземпляра, которые могут вызывать delete theproblemих деструктор).

Если это так, то Derivedэто должно быть не подлежащим копированию и не подлежащим назначению. Конечно, если вы делаете закрытую копию IAbstract, так как Derivedона нужна по умолчанию , Derivedона также не будет копироваться. Но если вы определяете свое собственное Derived::Derived(const Derived&)без вызова IAbtractcopy, вы все равно можете копировать их.

Проблема в Derived, и решение должно оставаться в Derived: если это должен быть динамический объект, доступ к которому есть только по указателям или ссылкам, то это должен быть сам Derived, который должен иметь

class Derived
{
    ...
    Derived(const Derived&) = delete;
    Derived& operator=(const Derived&) = delete;
};

По сути, именно разработчик класса Derived (который должен знать, как работает Derived и как theproblemуправляется), должен решить, что делать с назначением и копированием.

Эмилио Гаравалья
источник