Яка схема безпечного інтерфейсу в C ++


22

Примітка. Далі йде код 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() {}
} ;

Це рішення є простим і дещо наївним: воно не відповідає усім нашим обмеженням: воно може бути створене за замовчуванням, сконфігуровано та призначено для копіювання (я навіть не впевнений у конструкторах переміщення та призначенні, але у мене ще два роки, щоб зрозуміти це виходить).

  1. Ми не можемо оголосити деструктор чистим віртуальним, тому що нам потрібно підтримувати його в Inline, а деякі наші компілятори не перетравлюють чисті віртуальні методи з вбудованим порожнім тілом.
  2. Так, єдиний пункт цього класу - зробити реалізатори практично руйнівними, що є рідкісним випадком.
  3. Навіть якби у нас був додатковий чистий віртуальний метод (який є більшістю випадків), цей клас все ще може бути призначений для копіювання.

Отже, ні ...

1-е рішення: boost :: не можна скопіювати

class VirtuallyDestructible : boost::noncopyable
{
   public :
      virtual ~VirtuallyDestructible() {}
} ;

Це рішення є найкращим, оскільки воно є простим, зрозумілим та C ++ (без макросів)

Проблема полягає в тому, що він все ще не працює для цього конкретного інтерфейсу, оскільки VirtualConstructible все ще може бути створений за замовчуванням .

  1. Ми не можемо оголосити деструктора чистим віртуальним, оскільки нам потрібно підтримувати його в Inline, а деякі наші компілятори не перетравлюють його.
  2. Так, єдиний пункт цього класу - зробити реалізатори практично руйнівними, що є рідкісним випадком.

Інша проблема полягає в тому, що класи, що реалізують інтерфейс, який не можна скопіювати, повинні явно декларувати / визначати конструктор копій та оператор присвоєння, якщо їм потрібно мати ці методи (і в нашому коді у нас є класи цінностей, до яких клієнт все ще може отримати доступ через інтерфейси).

Це суперечить Правилу нуля, саме там ми хочемо піти: Якщо реалізація за замовчуванням нормальна, ми повинні мати можливість її використовувати.

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 :

Це життєздатне рішення моєї проблеми з наступних причин:

  • Цей клас неможливо встановити (конструктори захищені)
  • Цей клас можна практично знищити
  • Цей клас може бути успадкований без накладення надмірних обмежень для класів успадкування (наприклад, клас успадкування може бути за замовчуванням копіюється)
  • Використання макросу означає, що інтерфейс "декларація" легко впізнається (і можна шукати), а його код враховується в одному місці, що полегшує його модифікацію (відповідне префіксне ім'я видалить небажані сутички імен)

Зауважте, що інші відповіді дали цінні уявлення. Дякую всім вам, хто пострілив.

Зауважте, що я думаю, що я все-таки можу поставити ще одне щедрість щодо цього питання, і я ціную підсвічування відповідей достатньою мірою, якщо я бачу її, я би відкрив щедрість просто для того, щоб призначити її цій відповіді.


5
Ви не можете просто використовувати чисті віртуальні функції в інтерфейсі? virtual void bar() = 0;наприклад? Це не дозволить інстанціювати ваш інтерфейс.
Морвенн

@Morwenn: Як сказано в питанні, це вирішило б 99% випадків (я прагну на 100%, якщо це можливо). Навіть якщо ми вирішимо ігнорувати пропущений 1%, це також не вирішить розрізання завдання. Отже, ні, це не гарне рішення.
paercebal

@Morwenn: Серйозно? ... :-D ... Спочатку я написав це запитання на StackOverflow, а потім передумав перед тим, як подати його. Чи вважаєте ви, що я повинен її видалити тут і надіслати на SO?
paercebal

Якщо я маю рацію, то все, що вам потрібно, це virtual ~VirtuallyDestructible() = 0і віртуальне успадкування класів інтерфейсу (лише з абстрактними членами). Ви можете опустити це VirtualDestructible, ймовірно.
Дітер Люккінг

5
@paercebal: Якщо компілятор задихається від чистих віртуальних класів, він належить до кошика. Справжній інтерфейс за визначенням чистий віртуальний.
Ніхто

Відповіді:


13

Канонічним способом створення інтерфейсу в C ++ є надання йому чистого віртуального деструктора. Це забезпечує це

  • Жодних екземплярів самого інтерфейсного класу неможливо створити, оскільки C ++ не дозволяє створити екземпляр абстрактного класу. Це турбується про неконструктивні вимоги (і за замовчуванням, і для копіювання).
  • Виклик deleteвказівника на інтерфейс робить правильно: він викликає деструктор найбільш похідного класу для цього екземпляра.

просто наявність чистого віртуального деструктора не перешкоджає призначенню посилання на інтерфейс. Якщо вам це теж потрібно, ви повинні додати до свого інтерфейсу захищений оператор призначення.

Будь-який компілятор C ++ повинен мати можливість керувати таким класом / інтерфейсом, як цей (усе в одному файлі заголовка):

class MyInterface {
public:
  virtual ~MyInterface() = 0;
protected:
  MyInterface& operator=(const MyInterface&) { return *this; } // or = default for C++14
};

inline MyInterface::~MyInterface() {}

Якщо у вас є компілятор, який задушує це (а це означає, що він повинен бути попереднім C ++ 98), то ваш варіант 2 (із захищеними конструкторами) є хорошим другим-найкращим.

Використання boost::noncopyableне є доцільним для цього завдання, оскільки воно надсилає повідомлення про те, що всі класи в ієрархії повинні бути не скопійованими, і це може створити плутанину для більш досвідчених розробників, які не були б ознайомлені з вашими намірами щодо його використання.


If you need [prevent assignment] to fail as well, then you must add a protected assignment operator to your interface.: Це корінь моєї проблеми. Випадки, коли мені потрібен інтерфейс для підтримки призначення, справді мають бути рідкісними. З іншого боку, випадків, коли я хочу передати інтерфейс за посиланням (випадки, коли NULL неприйнятний), і, таким чином, хочу уникнути неоперативного або розрізання цього компіляції набагато більше.
paercebal

Оскільки оператор призначення не повинен називатися, чому ви даєте йому визначення? Як убік, чому б не зробити це private? Крім того, ви можете мати справу з замовчуванням і копіювальним механізмом.
Дедуплікатор

5

Я параноїк ...

  • Я параноїдально хочу, щоб код був захищений від зрізання в інтерфейсах? (Я вірю, що ні, але ніколи не знаю ...)

Це не проблема управління ризиками?

  • ти боїшся, що ймовірно, що введено помилку, пов’язану з нарізкою?
  • ви вважаєте, що це може залишитися непоміченим і спровокувати непоправні помилки?
  • наскільки ви готові піти, щоб уникнути нарізки?

Найкраще рішення

  • Яке найкраще рішення серед наведених вище?

Ваше друге рішення ("зробіть їх захищеними") виглядає добре, але майте на увазі, що я не є експертом C ++.
Принаймні, моїй компілятор видає неправильні звичаї, як неправильно, як помилкові (g ++).

Тепер вам потрібні макроси? Я б сказав "так", тому що, хоча ви й не говорите, яка мета написаного вами керівництва, я думаю, це полягає в застосуванні певного набору найкращих практик у коді вашого продукту.

З цією метою макроси можуть допомогти виявити, коли люди ефективно застосовують шаблон: базовий фільтр комітетів може повідомити, чи використовувався макрос:

  • якщо він використовується, то, швидше за все, шаблон буде застосований і, що ще важливіше, правильно застосований (просто перевірте, чи є protectedключове слово),
  • якщо він не використовується, ви можете спробувати дослідити, чому цього не було.

Без макросів ви повинні перевірити, чи потрібна і добре реалізована модель у всіх випадках.

Краще рішення

  • Чи є інше, краще рішення?

Нарізання C ++ - це не що інше, як особливість мови. Оскільки ви пишете керівництво (особливо для новачків), вам слід зосередитись на навчанні, а не просто перерахуванні "правил кодування". Ви повинні переконатися, що ви дійсно пояснюєте, як і чому відбувається нарізка, разом із прикладами та вправами (не винаходити колесо, отримувати натхнення з книг та навчальних посібників).

Наприклад, назва вправи може бути " Яка схема безпечного інтерфейсу в C ++ ?"

Отже, вашим найкращим кроком було б забезпечити, щоб ваші розробники C ++ розуміли, що відбувається, коли відбувається нарізка. Я переконаний, що якщо вони зроблять, вони не зроблять стільки помилок у коді, скільки ви побоюєтесь, навіть не формально виконуючи цей конкретний шаблон (але ви все одно можете його застосувати, попередження компілятора хороші).

Про компілятор

Ти кажеш :

У мене немає сили на вибір компіляторів для цього продукту,

Часто люди скажуть "я не маю права робити [X]" , "я не повинен робити це [Y] ..." , ... тому що вони думають, що це неможливо, і не тому, що вони спробував чи запитав.

Можливо, це частина вашої посадової інструкції, щоб висловити свою думку щодо технічних питань; якщо ви дійсно вважаєте, що компілятор є ідеальним (або унікальним) вибором для вашої проблемної області, тоді використовуйте його. Але ви також сказали, що "чисті віртуальні деструктори з вбудованою реалізацією - це не найгірший момент задушення, який я бачив" ; наскільки я розумію, компілятор настільки особливий, що навіть обізнані розробники C ++ мають труднощі з його використанням: ваш застарілий / власний компілятор зараз технічний борг, і ви маєте право (обов'язок?) обговорювати це питання з іншими розробниками та менеджерами .

Спробуйте оцінити витрати на утримання компілятора порівняно з витратами на використання іншого:

  1. Що приносить вам поточний компілятор, який ніхто не може?
  2. Чи легко поєднується ваш код продукту за допомогою іншого компілятора? Чому ні ?

Я не знаю вашої ситуації, і ви, мабуть, маєте поважні причини бути прив’язаними до конкретного компілятора.
Але якщо це просто інерція, ситуація ніколи не розвиватиметься, якщо ви або ваші колеги не повідомлять про проблеми з продуктивністю чи технічними боргами.


Am I paranoid...: "Зробіть інтерфейси простими у використанні та важко використовувати неправильно". Я відчув той особливий принцип, коли хтось повідомив про один із моїх статичних методів, помилково використовувався неправильно. Виникла помилка, здавалося, не пов'язана, і інженеру знадобилося кілька годин, щоб знайти джерело. Ця "помилка інтерфейсу" збігається з присвоєнням посилання інтерфейсу іншому. Отже, так, я хочу уникати подібних помилок. Крім того, в C ++ філософія полягає в тому, щоб якомога більше спіймати час компіляції, і мова дає нам таку силу, тому ми йдемо з нею.
paercebal

Best solution: Я згоден. . . Better solution: Це приголомшлива відповідь. Я попрацюю над цим ... А тепер про Pure virtual classes: Що це? Абстрактний інтерфейс C ++? (клас без стану і лише чисті віртуальні методи?). Як цей "чистий віртуальний клас" захистив мене від нарізки? (чисті віртуальні методи змусять екземпляр не компілювати, але присвоєння копії буде, і переміщення призначення теж буде IIRC).
paercebal

About the compiler: Ми погоджуємося, але наші компілятори перебувають поза моєю відповідальністю (не те, що це заважає мені зауважувати коментарі ... :-p ...). Я не розголошу подробиці (хотілося б, щоб), але вона пов'язана з внутрішніми причинами (наприклад, тестовими наборами) та зовнішніми причинами (наприклад, зв'язок клієнта з нашими бібліотеками). Зрештою, зміна версії компілятора (або навіть її виправлення) НЕ є тривіальною операцією. Не кажучи вже про те, щоб замінити один зламаний компілятор на останній gcc.
paercebal

@paercebal дякую за ваші коментарі; про чисті віртуальні класи, ви праві, це не вирішує всіх ваших обмежень (я цю частину видалю). Я розумію частину "помилки інтерфейсу" і як корисні помилки під час компіляції корисні: але ви запитали, чи є ви параноїком, і я вважаю, що раціональний підхід полягає в тому, щоб збалансувати вашу потребу в статичних перевірках з ймовірністю виникнення помилки. Удачі з справою компілятора :)
coredump

1
Я не прихильник макросів, тим більше, що настанови орієнтовані (також) на юнаків. Занадто часто я бачив людей, яким надавали такі «зручні» інструменти, щоб сліпо застосовувати їх і ніколи не розуміти, що відбувається насправді. Вони приходять до думки, що те, що робить макрос, має бути найскладнішою справою, оскільки їхній начальник вважав, що їм буде занадто складно зробити самі. Оскільки макрос існує лише у вашій компанії, вони навіть не можуть здійснити пошук в Інтернеті, тоді як для документального підтвердження, які функції учасників оголосити і чому, вони могли б.
5gon12eder

2

Проблема нарізки - це одна, але, безумовно, не єдина, яка представлена, коли ви піддаєте користувачам поліморфний інтерфейс під час роботи. Подумайте про нульові покажчики, управління пам’яттю, спільні дані. Жоден із них легко вирішити у всіх випадках (розумні покажчики чудові, але навіть вони не є жодною срібною кулею). Насправді, з вашого допису не здається, що ви намагаєтеся вирішити проблему зрізання, а скоріше пройдіть її, не дозволяючи користувачам робити копії. Все, що вам потрібно зробити, щоб запропонувати рішення проблеми нарізки - це додати функцію віртуального члена-клона. Я думаю, що більш глибока проблема розкриття поліморфного інтерфейсу під час виконання полягає в тому, що ви змушуєте користувачів працювати з еталонною семантикою, яку важче обґрунтувати, ніж значення семантики.

Найкращий спосіб, який я знаю, щоб уникнути цих проблем на C ++ - це використовувати стирання типу . Це техніка, коли ви ховаєте поліморфний інтерфейс під час виконання, за інтерфейсом звичайного класу. Цей звичайний інтерфейс класу має значущу семантику і піклується про всі поліморфні «безладдя» за екранами. std::functionє яскравим прикладом стирання типу.

Чудове пояснення того, чому виставляти спадщину своїм користувачам погано, і як стирання типів може допомогти виправити, які бачать ці презентації від Sean Parent:

Спадщина - це базовий клас зла (коротка версія)

Поліморфізм на основі цінової семантики та концепцій (довга версія; легше дотримуватися, але звук не великий)


0

Ти не параноїк. Моє перше професійне завдання як програміста на C ++ призвело до розрізання та збиття. Я знаю інших. Для цього не так багато хороших рішень.

Враховуючи обмеження компілятора, варіант 2 найкращий. Замість того, щоб робити макрос, який ваші нові програмісти вважатимуть дивним та загадковим, я запропонував би сценарій чи інструмент для автоматичного генерування коду. Якщо ваші нові співробітники будуть використовувати IDE, ви повинні мати можливість створити інструмент "Новий інтерфейс MYCOMPANY", який запитає ім'я інтерфейсу та створить структуру, яку ви шукаєте.

Якщо ваші програмісти використовують командний рядок, тоді використовуйте будь-яку мову скриптів, яка вам доступна, щоб створити сценарій NewMyCompanyInterface для створення коду.

Раніше я використовував такий підхід для поширених моделей коду (інтерфейси, стан машини тощо). Приємна частина полягає в тому, що нові програмісти можуть прочитати вихід і зрозуміти його легко, відтворюючи необхідний код, коли їм потрібно щось, що неможливо створити.

Макроси та інші підходи до метапрограмування, як правило, притуплюють те, що відбувається, і нові програмісти не дізнаються, що відбувається "за завісою". Коли їм доводиться ламати візерунок, вони так само втрачаються, як і раніше.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.