Чому «чистий поліморфізм» кращий, ніж використання RTTI?


106

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

Деякі компілятори не використовують його / RTTI не завжди вмикається

Я справді не купую цей аргумент. Це як би сказати, що я не повинен використовувати функції C ++ 14, оскільки там є компілятори, які не підтримують його. І все ж, ніхто не заважав би мені використовувати функції C ++ 14. Більшість проектів матиме вплив на компілятор, який вони використовують, і на те, як він налаштований. Навіть цитуючи сторінку gcc:

-fno-rtti

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

Це мені говорить, що якщо я не використовую RTTI, я можу його відключити. Це як би сказати, якщо ви не використовуєте Boost, вам не доведеться посилатися на нього. Мені не потрібно планувати випадок, коли хтось збирається -fno-rtti. Плюс у цьому випадку компілятор вийде з ладу голосно і чітко.

Це коштує додаткової пам'яті / Може бути повільним

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

Дійсна передача може бути повільною. Зазвичай існують способи уникнути необхідності використання в ньому критичних для швидкості ситуацій. І я не зовсім бачу альтернативи. Ця відповідь ТА пропонує використовувати enum, визначений у базовому класі, для зберігання типу. Це працює лише в тому випадку, якщо ви знаєте всі похідні класи a-priori. Це досить велике "якщо"!

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

Елегантні поліморфні конструкції зроблять RTTI непотрібним

Це такий тип порад, який я сприймаю серйозно. У цьому випадку я просто не можу придумати хороших не-RTTI-рішень, які стосуються мого випадку використання RTTI. Наведіть приклад:

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

class node_base
{
  public:
    node_base();
    virtual ~node_base();

    std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();
};

Тепер мої вузли можуть бути різного типу. Як щодо цього:

class red_node : virtual public node_base
{
  public:
    red_node();
    virtual ~red_node();

    void get_redness();
};

class yellow_node : virtual public node_base
{
  public:
    yellow_node();
    virtual ~yellow_node();

    void set_yellowness(int);
};

Чорт, чому навіть не один із них:

class orange_node : public red_node, public yellow_node
{
  public:
    orange_node();
    virtual ~orange_node();

    void poke();
    void poke_adjacent_oranges();
};

Остання функція цікава. Ось спосіб написати це:

void orange_node::poke_adjacent_oranges()
{
    auto adj_nodes = get_adjacent_nodes();
    foreach(auto node, adj_nodes) {
        // In this case, typeid() and static_cast might be faster
        std::shared_ptr<orange_node> o_node = dynamic_cast<orange_node>(node);
        if (o_node) {
             o_node->poke();
        }
    }
}

Це все здається чітким і чистим. Мені не потрібно визначати атрибути чи методи там, де вони мені не потрібні, клас базового вузла може залишатися худорлявим і середнім. Без RTTI, з чого почати? Можливо, я можу додати атрибут node_type до базового класу:

class node_base
{
  public:
    node_base();
    virtual ~node_base();

    std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();

  private:
    std::string my_type;
};

Чи хороша ідея для типу std :: string? Можливо, ні, але що ще я можу використовувати? Складіть номер і сподіваєтесь, що його ще ніхто не використовує? Крім того, що стосується мого orange_node, що робити, якщо я хочу використовувати методи з red_node та yellow_node? Чи потрібно мені зберігати кілька типів на вузол? Це здається складним.

Висновок

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

Отже, повернутися до мого початкового запитання: Чому перевагу «чистого поліморфізму» краще використовувати RTTI?


9
Те, що вам "не вистачає" (як мовна особливість) для вирішення вашого прикладу апельсинів тхну, буде багаторазовою відправленням ("мультиметодами"). Таким чином, шукаємо способи наслідування, які могли б бути альтернативою. Зазвичай використовується шаблон відвідувачів.
Даніель Жур

1
Використання рядка як типу не дуже корисно. Використання вказівника на екземпляр якогось класу "типу" зробить це швидше. Але тоді ви в основному робите вручну, що буде робити RTTI.
Даніель Жур

4
@MargaretBloom Ні, не буде, RTTI розшифровується як інформація про тип виконання, в той час як CRTP призначений лише для шаблонів - статичні типи.
edmz

2
@ mbr0wn: усі інженерні процеси пов'язані деякими правилами; програмування не є винятком. Правила можна розділити на два відра: м'які правила (ДОЛЖНІ) та жорсткі правила (ПОВИНЕН). (Є, так би мовити, відрада з порадами / опціями (COULD).) Прочитайте, як стандарт C / C ++ (або будь-який інший англомовний стандарт) визначає їх. Я думаю, що ваша проблема пов'язана з тим, що ви помилилися "не використовуйте RTTI" як жорстке правило ("ви НЕ МОЖЕТЕ використовувати RTTI"). Це насправді м'яке правило ("НЕ МОЖЕТЕ використовувати RTTI"), це означає, що вам слід уникати цього, коли це можливо - і просто користуватися, коли не можете цього зробити

3
Я зазначаю, що багато відповідей не відзначають ідею, що ваш приклад передбачає, що node_baseє частиною бібліотеки, і користувачі будуть робити власні типи вузлів. Тоді вони не можуть змінюватись, node_baseщоб дозволити інше рішення, тому, можливо, RTTI тоді стане їх найкращим варіантом. З іншого боку, є й інші способи проектування такої бібліотеки, щоб нові типи вузлів могли вміщуватися набагато більш елегантно, не потребуючи використання RTTI (та інших способів проектування нових типів вузлів).
Меттью Уолтон

Відповіді:


69

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

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

Тепер усі, хто поєднується з "помаранчевістю", поєднуються з усім вашим помаранчевим типом, а неявно - з усім вашим приватним садом, а не з визначеним інтерфейсом.

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

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

class node_base {
  public:
    bool has_tag(tag_name);

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

Це навіть можна поширити на динамічні методи, якщо вам це потрібно. "Чи підтримуєте ви бути Фуод з аргументами Баз, Тома та Еліс? Добре, обдуривши вас". У великому сенсі це менш нав'язливо, ніж динамічний акторський склад, щоб отримати факт, що інший об'єкт - це тип, який ви знаєте.

Тепер мандаринові об’єкти можуть мати помаранчевий тег і грати разом, будучи при цьому відокремленими.

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

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


14
+1 за самий останній абзац; не тільки тому, що я з вами згоден, а тому, що це тут молоток по цвяху.

7
Як можна отримати певну функціональність, коли дізнається, що об’єкт позначений як підтримка цієї функціональності? Або це включає кастинг, або є клас Бога з усіма можливими функціями членів. Перша можливість - це або неперевірений кастинг, і в цьому випадку тегування є лише власною дуже помилковою схемою перевірки динамічного типу, або вона перевірена dynamic_cast(RTTI), і в цьому випадку теги є зайвими. Друга можливість, клас Бога, є огидною. Підводячи підсумки, у цій відповіді є багато слів, які, на мою думку, звучать приємно для програмістів Java, але фактичний зміст є безглуздим.
ура та хт. - Альф

2
@Falco: Це (один варіант) перша можливість, яку я згадав, неперевірений кастинг на основі тегу. Тут маркування є власною крихкою і дуже помилковою схемою перевірки динамічного типу. Будь-який маленький неправильний поведінку коду клієнта, а в C ++ один вимкнено в UB-land. Ви не отримуєте винятків, як це можливо в Java, але не визначена поведінка, як збої та / або неправильні результати. Окрім того, що є надзвичайно ненадійним та небезпечним, він також є надзвичайно неефективним, порівняно із більш розумним кодом C ++. IOW., Це дуже дуже недоброзичливо; надзвичайно так.
ура та хт. - Альф

1
Ум. :) Типи аргументів?
ура та хт. - Альф

2
@JojOatXGME: Тому що "поліморфізм" означає вміння працювати з різними типами. Якщо вам доведеться перевірити, чи йдеться про певний тип, за винятком вже існуючої перевірки типу, яку ви використовували для отримання покажчика / посилання для початку, то ви дивитесь на поліморфізм. Ви не працюєте з різними типами; ви працюєте з певним типом. Так, у Java є "(великі) проекти", які роблять це. Але це Java ; мова дозволяє лише динамічний поліморфізм. C ++ також має статичний поліморфізм. Крім того, тільки те, що хтось "великий" робить це, це не робить його гарною ідеєю.
Нікол Болас

31

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

Якщо моралістів не вдається, це те, що вони вважають, що ВСЕ звичаї помилково сприймаються, а насправді функції існують не просто так.

У них є те , що я мав звичай називати «Водопровідник комплексом»: вони думають , що все крани мають неточності , тому що всі крани вони покликані на ремонт. Реальність полягає в тому, що більшість кранів працює добре: ви просто не зателефонуйте до них сантехніку!

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

Існує загальний спосіб думати про поліморфізм: IF(selection) CALL(something) WITH(parameters). (Вибачте, але все це стосується програмування, якщо нехтувати абстракцією)

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

Ідея полягає в тому, що:

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

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

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

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

Якщо комутація є багатовимірною, оскільки в C ++ не існує жодної багаторазової багаторазової розсилки , вам доведеться виконати:

  • Звести до одного виміру за допомогою Goedelization : саме там є віртуальні основи та багатократне успадкування, алмази та складені паралелограми , але для цього потрібно знати кількість можливих комбінацій та бути порівняно невеликим.
  • Пов'язуйте розміри один на інший (як у схемі композитних відвідувачів, але для цього потрібно, щоб усі класи усвідомлювали інших своїх побратимів, тому він не може «масштабувати» місця, де він був задуманий)
  • Диспетчерські дзвінки на основі кількох значень. Саме це і є RTTI.

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

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

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

Збивати RTTI просто для того, щоб розбити його, це як gotoбити просто, щоб розбити його. Речі для папуг, а не програмістів.


Гарне врахування рівнів, на яких застосовується кожен підхід. Я не чув про "Goedelization", хоча це також відомо іншим ім'ям? Не могли б ви додати посилання чи більше пояснень? Дякую :)
j_random_hacker

1
@j_random_hacker: Мені теж цікаво таке використання Godelization. Один звичайно думає про Godelization як перше, зіставлення з якогось рядка на якесь ціле число, а по-друге, використовуючи цю техніку для створення самореференційних висловлювань на офіційних мовах. Я не знайомий з цим терміном в контексті віртуальної відправки і хотів би дізнатися більше.
Ерік Ліпперт

1
Насправді я зловживаю терміном: за Гедле, оскільки кожне ціле число відповідає цілому n-ple (повноваження його простих факторів) і кожне n-ple відповідає цілому числу, кожна дискретна n-мірна задача індексації може бути зводиться до одновимірного . Це не означає, що це єдиний і єдиний спосіб зробити це: це просто спосіб сказати "можна". Все, що вам потрібно - це механізм «розділити і перемогти». віртуальні функції - це «розділити», а багаторазове успадкування - це «підкорити».
Еміліо Гаравалья

... Коли все, що відбувається всередині кінцевого поля (діапазону), лінійні комбінації є більш ефективними (класичний i = r * C + c, що отримує індекс у масиві комірки матриці). У цьому випадку розділення ідентифікатора "відвідувач" і підкорення - це "композит". Оскільки задіяна лінійна алгебра, методика в цьому випадку відповідає «діагоналізації»
Еміліо Гаравалія

Не думаю, що все це як техніка. Вони лише аналогії
Еміліо Гаравалья

23

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

Що про dark_orange_node, або black_and_orange_striped_node, або dotted_node? Чи може вона мати крапки різних кольорів? Що робити, якщо більшість крапок є помаранчевим, то його можна просунути?

І кожного разу, коли вам доведеться додавати нове правило, вам доведеться переглянути всі poke_adjacentфункції та додавати ще й if-заяви.


Як завжди, важко створити загальні приклади, я вам це наведу.

Але якби я робив цей конкретний приклад, я би додав poke()учасника до всіх класів і дозволю деяким із них ігнорувати call ( void poke() {}), якщо вони не зацікавлені.

Звичайно, це було б навіть дешевше, ніж порівняння typeids.


3
Ви говорите "напевно", але що робить вас настільки впевненими? Це справді те, що я намагаюся з’ясувати. Скажімо, я перейменую orange_node на pokable_node, і вони єдині, кого я можу викликати poke (). Це означає, що мій інтерфейс повинен буде реалізувати метод poke (), який, скажімо, кидає виняток ("цей вузол не є можливим"). Це здається більш дорогим.
mbr0wn

2
Навіщо йому потрібно кидати виняток? Якщо ви піклувались про те, чи є інтерфейс "здатний до poke", просто додайте функцію "isPokeable" і зателефонуйте їй спочатку, перш ніж викликати функцію poke. Або просто робити те, що він каже, і "не робити нічого, на заняттях, що не мають сили".
Брендон

1
@ mbr0wn: Краще питання, чому ви хочете, щоб показові та безмобільні вузли мали спільний доступ до одного базового класу.
Нікол Болас

2
@NicolBolas Чому ви хочете, щоб дружні та ворожі монстри ділилися на один і той же базовий клас, або фокусувані та не фокусирувані елементи інтерфейсу, або клавіатури з клавіатурою та клавіатурами без нумерації?
користувач253751

1
@ mbr0wn Це звучить як модель поведінки. Базовий інтерфейс має два методи, supportsBehaviourі invokeBehaviourкожен клас може мати Список поведінки. Одна поведінка була б Poke і могла бути додана до списку підтримуваних поведінок усіма класами, які хочуть стати побожними.
Falco

20

Деякі компілятори не використовують його / RTTI не завжди вмикається

Я вважаю, що ви неправильно зрозуміли такі аргументи.

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

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

Це коштує додаткової пам'яті / Може бути повільним

Є багато речей, які ви не робите в гарячих петлях. Ви не виділяєте пам'ять. Ви не проходите повторення через пов'язані списки. І так далі. RTTI, безумовно, може бути ще однією з таких речей "не робіть цього тут".

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

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

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

Отже ... чому ви написали це неправильно для початку?

Мені не потрібно визначати атрибути чи методи там, де вони мені не потрібні, клас базового вузла може залишатися худорлявим і середнім.

З якою метою?

Ви кажете, що базовий клас "худий і злий". Але насправді ... це не існує . Це насправді нічого не робить .

Просто подивіться на ваш приклад: node_base. Що це? Здається, це річ, яка має суміжні інші речі. Це інтерфейс Java (попередній генеричний Java на той час): клас, який існує виключно для того, щоб користувачі могли передавати реальний тип. Можливо, ви додасте якусь основну функцію, наприклад суміжність (додає Java ToString), але це все.

Існує різниця між "худий і середній" і "прозорий".

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

Але те, що вони також роблять, - це завдати великого болю насправді робити нові речі навіть у системі. Розглянемо свою poke_adjacent_orangesфункцію. Що станеться, якщо хтось хоче lime_nodeтип, який може бути тикнути так само, як і orange_nodes? Ну, ми не можемо вийти lime_nodeз цього orange_node; в цьому немає сенсу.

Натомість ми повинні додати нове lime_nodeпохідне від node_base. Потім змініть ім'я poke_adjacent_orangesна poke_adjacent_pokables. А потім спробуйте зробити кастинг до orange_nodeта lime_node; який би твір не робив, - це те, про що ми ткнемся.

Втім, lime_nodeпотребує свого poke_adjacent_pokables . І цю функцію потрібно робити ті ж перевірки кастингу.

І якщо ми додамо третій тип, ми повинні не лише додати його власну функцію, але ми повинні змінити функції в двох інших класах.

Очевидно, тепер ви робите poke_adjacent_pokablesбезкоштовну функцію, щоб вона працювала для всіх них. Але що ви гадаєте, що трапиться, якщо хтось додасть четвертий тип і забуде додати його до цієї функції?

Привіт, мовчазна поломка . Здається, програма працює більш-менш добре, але це не так. Якби pokeбула фактична віртуальна функція, компілятор не зміг би, якби ви не перекрили чисту віртуальну функцію node_base.

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

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

Таким чином, він "шкідливий" так само, як і goto. Це корисний інструмент, яким не слід займатися. Але його використання має бути рідкісним у вашому коді.


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

Відповідь залежить від того, для чого базовий клас.

Прозорі базові класи, як-от node_base, просто використовують неправильний інструмент для проблеми. Зв'язані списки найкраще обробляються шаблонами. Тип вузла та суміжність забезпечуватиметься типом шаблону. Якщо ви хочете помістити поліморфний тип у список, можете. Просто використовуйте BaseClass*як Tу аргументі шаблону. Або улюблений розумний вказівник.

Але є й інші сценарії. Один - це тип, який робить багато речей, але має деякі додаткові частини. Конкретний екземпляр може реалізувати певні функції, а інший - ні. Однак дизайн таких типів зазвичай пропонує належну відповідь.

Клас "сутність" є прекрасним прикладом цього. Цей клас вже давно мучив розробників ігор. Концептуально він має гігантський інтерфейс, що живе на перетині майже десятка, цілком розрізнених систем. І різні сутності мають різні властивості. Деякі об'єкти не мають візуального зображення, тому їх функції відтворення нічого не роблять. І це все визначається під час виконання.

Сучасне рішення для цього - система компонентного стилю. Entityце просто ємність з набором компонентів з деяким клеєм між ними. Деякі компоненти необов’язкові; суб'єкт, який не має візуального подання, не має компонента "графіка". Суб'єкт, що не має AI, не має компонента "контролер". І так далі.

Суб'єкти в такій системі є лише вказівниками на компоненти, більшість їх інтерфейсу забезпечується шляхом прямого доступу до компонентів.

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

Це також допомагає слідувати принципу єдиної відповідальності. Такий клас складових компонентів несе відповідальність лише за те, щоб бути власником компонентів.


Від Меттью Уолтона:

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

Добре, давайте вивчимо це.

Щоб це мало сенс, те, що вам доведеться мати, - це ситуація, коли деяка бібліотека L надає контейнер або інший структурований власник даних. Користувач отримує можливість додавати дані до цього контейнера, повторювати його вміст тощо. Однак бібліотека насправді нічого не робить з цими даними; він просто керує своїм існуванням.

Але він навіть не керує своїм існуванням так сильно, як руйнуванням . Причина полягає в тому, що якщо ви очікуєте використовувати RTTI для таких цілей, ви створюєте класи, до яких L не знає. Це означає, що ваш код виділяє об'єкт і передає його L для управління.

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

На мові C ця схема написана void*, і її використання вимагає великої обережності, щоб не порушити її. У C ++ цей шаблон написано std::experimental::any(незабаром буде написано std::any).

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

Таким чином, замість того, щоб виходити orange_nodeз цього документа node_data, ви просто наклейте orangeвнутрішнє поле node_data's anymember. Кінцевий користувач витягує його та використовує any_castдля його перетворення orange. Якщо акторський склад не вдається, значить, це не було orange.

Тепер, якщо ви взагалі знайомі з впровадженням any, ви, швидше за все, скажете: "ей, почекай хвилину: any внутрішньо використовує RTTI для any_castроботи". На що я відповідаю: "... так".

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

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

Так називається any. Він використовує RTTI, але використання anyнабагато перевершує використання RTTI безпосередньо, оскільки воно відповідає правильній бажаній семантиці.


10

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

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

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

Тож як боротися з цим ... Залежно від вашої ситуації може мати сенс мати будь-які властивості, характерні для вузла, в окремі об'єкти (тобто, весь «помаранчевий» API може бути окремим об’єктом). Кореневий об'єкт може мати віртуальну функцію для повернення API "помаранчевого", повертаючи nullptr за замовчуванням для неоранжевих об'єктів.

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


6
Re: вартість продуктивності - я оцінив, що в нашому додатку dinamiльний канал <> коштує приблизно 2 мкс на процесорі 3 ГГц, що приблизно в 1000 разів повільніше, ніж перевірка перерахунку. (У нашої програми встановлений термін основного циклу 11,1 мс, тому ми дуже дбаємо про мікросекунди.)
Crashworks

6
Продуктивність сильно відрізняється між реалізаціями. GCC використовує порівняння вказівника typeinfo, яке швидко. MSVC використовує порівняння рядків, які не є швидкими. Однак метод MSVC буде працювати з кодом, пов'язаним з різними версіями бібліотек, статичними або DLL, де метод покажчика GCC вважає, що клас у статичній бібліотеці відрізняється від класу в спільній бібліотеці.
Зан Лінкс

1
@Crashworks Просто щоб мати тут повний запис: який компілятор (і яка версія) це був?
H. Guijt

@Crashworks надсилає запит на інформацію про те, який компілятор видав ваші спостережувані результати; Дякую.
підкреслюй_d

@underscore_d: MSVC.
Crashworks

9

C ++ побудований на ідеї перевірки статичного типу.

[1] RTTI, тобто dynamic_castі type_idє, динамічна перевірка типу.

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


Re

» Я не знав би , чистий спосіб зробити це з допомогою шаблонів або інших методів

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

#include <iostream>
#include <set>
#include <initializer_list>

namespace graph {
    using std::set;

    class Red_thing;
    class Yellow_thing;
    class Orange_thing;

    struct Callback
    {
        virtual void handle( Red_thing& ) {}
        virtual void handle( Yellow_thing& ) {}
        virtual void handle( Orange_thing& ) {}
    };

    class Node
    {
    private:
        set<Node*> connected_;

    public:
        virtual void call( Callback& cb ) = 0;

        void connect_to( Node* p_other )
        {
            connected_.insert( p_other );
        }

        void call_on_connected( Callback& cb )
        {
            for( auto const p : connected_ ) { p->call( cb ); }
        }

        virtual ~Node(){}
    };

    class Red_thing
        : public virtual Node
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }

        auto redness() -> int { return 255; }
    };

    class Yellow_thing
        : public virtual Node
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }
    };

    class Orange_thing
        : public Red_thing
        , public Yellow_thing
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }

        void poke() { std::cout << "Poked!\n"; }

        void poke_connected_orange_things()
        {
            struct Poker: Callback
            {
                void handle( Orange_thing& obj ) override
                {
                    obj.poke();
                }
            } poker;

            call_on_connected( poker );
        }
    };
}  // namespace graph

auto main() -> int
{
    using namespace graph;

    Red_thing   r;
    Yellow_thing    y1, y2;
    Orange_thing    o1, o2, o3;

    for( Node* p : std::initializer_list<Node*>{ &y1, &y2, &r, &o2, &o3 } )
    {
        o1.connect_to( p );
    }
    o1.poke_connected_orange_things();
}

Це передбачає, що набір типів вузлів відомий.

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


Для шаблонного підходу див. Бібліотеку Boost Graph. Сумно сказати, що я не знайомий з цим, я не використовував його. Тож я не впевнений, що саме робить і як, і в якій мірі він використовує статичну перевірку типу замість RTTI, але оскільки Boost, як правило, базується на шаблонах із статичною типовою перевіркою як центральною ідеєю, я думаю, ви знайдете це його підбібліотека Graph також заснована на перевірці статичного типу.


[1] Інформація про тип часу роботи .


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

3

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

if ( typeid(5) == "int" )
    // may be false

if ( typeid(5) == typeid(int) )
   // always true

Те саме стосується хешей.

[...] RTTI "вважається шкідливим"

шкідливий , безумовно, завищує: RTTI має деякі недоліки, але має і переваги.

Вам не потрібно використовувати RTTI. RTTI - це інструмент для вирішення проблем OOP: якщо ви використовуєте іншу парадигму, вони, ймовірно, зникнуть. C не має RTTI, але все ще працює. C ++ замість повністю підтримує ООП і дає вам кілька інструментів , щоб подолати деякі проблеми , які можуть зажадати виконання інформації: один з них є дійсно RTTI, який хоч і приходить з ціною. Якщо ви не можете собі цього дозволити, якщо вам краще заявити лише після безпечного аналізу продуктивності, все ще є стара школа void*: це безкоштовно. Безцінне. Але ви не отримаєте безпеку типу. Тож все стосується торгів.


  • Деякі компілятори не використовують / RTTI не завжди ввімкнено,
    я справді не купую цей аргумент. Це як би сказати, що я не повинен використовувати функції C ++ 14, оскільки там є компілятори, які не підтримують його. І все ж, ніхто не заважав би мені використовувати функції C ++ 14.

Якщо ви пишете (можливо, суворо) відповідний код C ++, ви можете очікувати такої ж поведінки незалежно від реалізації. Стандартні сумісні реалізації повинні підтримувати стандартні функції C ++.

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


Я погоджуюся з Yakk щодо RTTI в даному випадку. Так, це можна було б використовувати; але це логічно правильно? Те, що мова дозволяє обійти цю перевірку, не означає, що це слід робити.

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