Примітка. Далі йде код C ++ 03, але ми очікуємо перехід на C ++ 11 у наступні два роки, тому ми повинні пам’ятати про це.
Я пишу настанову (для новачків, серед інших) про те, як написати абстрактний інтерфейс на C ++. Я читав обидві статті Саттера на цю тему, шукав в Інтернеті приклади та відповіді та робив кілька тестів.
Цей код НЕ повинен складатись!
void foo(SomeInterface & a, SomeInterface & b)
{
SomeInterface c ; // must not be default-constructible
SomeInterface d(a); // must not be copy-constructible
a = b ; // must not be assignable
}
Усі вищезгадані форми поведінки знаходять джерело своєї проблеми в нарізанні : Абстрактний інтерфейс (або не-лист класу в ієрархії) не повинен бути конструктивним, не підлягати копіюванню / присвоєнню, НАКІЛЬКІ, якщо похідний клас може бути.
0-е рішення: базовий інтерфейс
class VirtuallyDestructible
{
public :
virtual ~VirtuallyDestructible() {}
} ;
Це рішення є простим і дещо наївним: воно не відповідає усім нашим обмеженням: воно може бути створене за замовчуванням, сконфігуровано та призначено для копіювання (я навіть не впевнений у конструкторах переміщення та призначенні, але у мене ще два роки, щоб зрозуміти це виходить).
- Ми не можемо оголосити деструктор чистим віртуальним, тому що нам потрібно підтримувати його в Inline, а деякі наші компілятори не перетравлюють чисті віртуальні методи з вбудованим порожнім тілом.
- Так, єдиний пункт цього класу - зробити реалізатори практично руйнівними, що є рідкісним випадком.
- Навіть якби у нас був додатковий чистий віртуальний метод (який є більшістю випадків), цей клас все ще може бути призначений для копіювання.
Отже, ні ...
1-е рішення: boost :: не можна скопіювати
class VirtuallyDestructible : boost::noncopyable
{
public :
virtual ~VirtuallyDestructible() {}
} ;
Це рішення є найкращим, оскільки воно є простим, зрозумілим та C ++ (без макросів)
Проблема полягає в тому, що він все ще не працює для цього конкретного інтерфейсу, оскільки VirtualConstructible все ще може бути створений за замовчуванням .
- Ми не можемо оголосити деструктора чистим віртуальним, оскільки нам потрібно підтримувати його в Inline, а деякі наші компілятори не перетравлюють його.
- Так, єдиний пункт цього класу - зробити реалізатори практично руйнівними, що є рідкісним випадком.
Інша проблема полягає в тому, що класи, що реалізують інтерфейс, який не можна скопіювати, повинні явно декларувати / визначати конструктор копій та оператор присвоєння, якщо їм потрібно мати ці методи (і в нашому коді у нас є класи цінностей, до яких клієнт все ще може отримати доступ через інтерфейси).
Це суперечить Правилу нуля, саме там ми хочемо піти: Якщо реалізація за замовчуванням нормальна, ми повинні мати можливість її використовувати.
2-е рішення: зробіть їх захищеними!
class MyInterface
{
public :
virtual ~MyInterface() {}
protected :
// With C++11, these methods would be "= default"
MyInterface() {}
MyInterface(const MyInterface & ) {}
MyInterface & operator = (const MyInterface & ) { return *this ; }
} ;
Ця закономірність відповідає технічним обмеженням (принаймні, в коді користувача): MyInterface не може бути створений за замовчуванням, не може бути створено для копіювання і не може бути призначено для копіювання.
Крім того, він не накладає штучних обмежень на реалізацію класів , які можуть вільно слідувати Правилу нуля або навіть оголошувати декількох конструкторів / операторів як "= за замовчуванням" в C ++ 11/14 без проблем.
Тепер це досить багатослівно, і альтернативою було б використання макросу, як-от:
class MyInterface
{
public :
virtual ~MyInterface() {}
protected :
DECLARE_AS_NON_SLICEABLE(MyInterface) ;
} ;
Захищений повинен залишатися поза макросом (оскільки він не має сфери застосування).
Правильно "простір імен" (тобто з префіксом назви вашої компанії чи товару) макрос повинен бути нешкідливим.
А перевага полягає в тому, що код використовується в одному джерелі, а не копіюється в усі інтерфейси. Якщо конструктор переміщення та призначення переміщення будуть явно відключені однаково в майбутньому, це буде дуже легкою зміною коду.
Висновок
- Я параноїдально хочу, щоб код був захищений від зрізання в інтерфейсах? (Я вірю, що ні, але ніколи не знаю ...)
- Яке найкраще рішення серед наведених вище?
- Чи є інше, краще рішення?
Будь ласка, пам’ятайте, що це шаблон, який слугуватиме орієнтиром для новачків (серед інших), тому таке рішення, як: «Кожен випадок повинен мати свою реалізацію», не є прийнятним рішенням.
Багатство та результати
Я нагородив винагороду за велику суму за час, витрачений на відповіді на запитання, та відповідність відповідей.
Моє рішення проблеми, ймовірно, піде на щось подібне:
class MyInterface
{
DECLARE_CLASS_AS_INTERFACE(MyInterface) ;
public :
// the virtual methods
} ;
... із таким макросом:
#define DECLARE_CLASS_AS_INTERFACE(ClassName) \
public : \
virtual ~ClassName() {} \
protected : \
ClassName() {} \
ClassName(const ClassName & ) {} \
ClassName & operator = (const ClassName & ) { return *this ; } \
private :
Це життєздатне рішення моєї проблеми з наступних причин:
- Цей клас неможливо встановити (конструктори захищені)
- Цей клас можна практично знищити
- Цей клас може бути успадкований без накладення надмірних обмежень для класів успадкування (наприклад, клас успадкування може бути за замовчуванням копіюється)
- Використання макросу означає, що інтерфейс "декларація" легко впізнається (і можна шукати), а його код враховується в одному місці, що полегшує його модифікацію (відповідне префіксне ім'я видалить небажані сутички імен)
Зауважте, що інші відповіді дали цінні уявлення. Дякую всім вам, хто пострілив.
Зауважте, що я думаю, що я все-таки можу поставити ще одне щедрість щодо цього питання, і я ціную підсвічування відповідей достатньою мірою, якщо я бачу її, я би відкрив щедрість просто для того, щоб призначити її цій відповіді.
virtual ~VirtuallyDestructible() = 0і віртуальне успадкування класів інтерфейсу (лише з абстрактними членами). Ви можете опустити це VirtualDestructible, ймовірно.
virtual void bar() = 0;наприклад? Це не дозволить інстанціювати ваш інтерфейс.