Правило 27 не зловживайте приведенням типів - ефективне використання c

Правило 27: Не зловживайте приведенням типів

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

На жаль, приведення обходять систему типів. І це може призвести до різних проблем, деякі з яких розпізнати легко, а деякі - надзвичайно важко. Якщо ви прийшли до C ++ зі світу C, Java або C #, прийміть Етока відома, оскільки в зазначених мовах в приведення типів частіше виникає необхідність, і вони менш небезпечні, ніж в C ++. Але C ++ - це не C. Це не Java. Це не C #. У цій мові приведення - це засіб, до якого потрібно ставитися з належною повагою.

Почнемо з огляду синтаксису операторів приведення типів, тому що існує три різні способи написати одне й те саме. Приведення в стилі C виглядає так:

(T) expression // привести expression до типу T

Функціональний синтаксис приведення такий:

T (expression) // привести expression до типу T

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

C ++ також представляє чотири нові форми приведення типів (часто звані привидами в стилі С ++):

У кожної з них своє призначення:

• const_cast зазвичай застосовується для того, щоб відкинути константность об'єкта. Ніяке інше приведення в стилі C ++ не дозволяє це зробити;

• dynamic_cast застосовується головним чином для виконання «безпечного понижуючого приведення» (downcasting). Цей оператор дозволяє визначити, чи належить об'єкт даного типу деякої ієрархії успадкування. Це єдиний вид приведення, який не може бути виконаний з використанням старого синтаксису. Це також єдине приведення, яке може зажадати суттєвих витрат під час виконання (докладніше пізніше);

• reinterpret_cast призначений для низькорівневих привидів, які породжують залежні від реалізації (тобто нестерпні) результати, наприклад приведення покажчика до int. Поза низкоуровневого коду таке приведення повинно використовуватися рідко. Я використовував його в цій книзі лише одного разу, коли обговорював написання отладочного розподільника пам'яті (див. Правило 50);

• static_cast може бути використаний для явного перетворення типів (наприклад, неконстантних об'єктів до сталою (як в правилі 3), int до double і т. П.). Він також може бути використаний для виконання зворотних перетворень (наприклад, покажчиків void * до типізованим вказівниками, покажчиків на базовий клас до покажчика на похідний). Але привести константний об'єкт до неконстантному цей оператор не може (це вотчина const_cast).

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

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

explicit Widget (int size);

void doSomeWork (const Widget w);

doSomeWork (Widget (15)); // створити Widget з int

// з функціональним приведенням

doSomeWork (static_cast (15)); // створити Widget з int

// з приведенням у стилі C ++

Але навмисне створення об'єкта не «відчувається» як приведення типу, тому в даному випадку, напевно, краще застосувати функціональне приведення замість static_cast. Та й взагалі, код, що веде до аварійного завершення, зазвичай виглядає цілком розумним, коли ви його пишете, тому краще не звертати уваги на відчуття і завжди користуватися привидами в новому стилі.

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

double d = static_cast (x) / y; // розподіл x на y з використанням

// ділення з плаваючою точкою

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

class Derived: public Base;

Base * pb = d; // неявне перетворення Derived *

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

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

class Window

virtual void onResize () // реалізація onResize в базовому

class SpecialWindow: public Window

virtual void onResize ()

static_cast (* this) .onResize (); // в похідному класі;

// приведення * this до Window,

// потім виклик його onResize;

// це не працює!

// виконання специфічної для

> // SpecialWindow частини onResize

Я виділив в цьому коді приведення типу. (Це приведення в новому стилі, але використання старого стилю нічого не змінює.) Як і очікується, * this призводить до типу Window. Тому звернення до onResize призводить до виклику Window :: onResize. Ось тільки ця функція не викликана для поточного об'єкта! Несподівано, чи не так? Замість цього оператор приведення створить нову, тимчасову копію частини базового класу * this і викличе onResize для цієї копії! Наведений вище код не викличе Window :: onResize для поточного об'єкта з подальшим виконанням специфічних для SpecialWindow дій - він виконає Window :: onResize для копії частини базового класу поточного об'єкта перед виконанням специфічних для SpecialWindow дій для даного об'єкта. Якщо Window :: onResize модифікує об'єкт (що цілком можливо, так як onResize - НЕ константная функція-член), то поточний об'єкт не буде модифікований. Замість цього буде модифікована копія цього об'єкта. Однак якщо SpecialWindow :: onResize модифікує об'єкт, то буде модифікований саме поточний об'єкт. І в результаті поточний об'єкт залишається в неузгоджену стані, тому що модифікація тієї його частини, що належить базовому класу, нічого очікувати виконано, а модифікація частини, що належить похідному класу, буде.

Рішення проблеми в тому, щоб виключити приведення типу, замінивши його тим, що ви дійсно мали на увазі. Немає необхідності виконувати якісь трюки з компілятором, змушуючи його інтерпретувати * this як об'єкт базового класу. Ви хочете викликати версію onResize базового класу для поточного об'єкта. Так поступите таким чином:

class SpecialWindow: public Window

virtual void onResize ()

Window :: onResize (); // виклик Window :: onResize на * this

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

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

Перший - використовуйте контейнери для зберігання покажчиків (часто «інтелектуальних», див. Правило 13) на самі об'єкти похідних класів, тоді відпаде необхідність маніпулювати цими об'єктами через інтерфейси базового класу. Наприклад, якщо в нашій ієрархії Window / SpecialWindow тільки SpecialWindow підтримує мерехтіння (blinking), то замість:

typedef // см. правило 13

std :: vector> VPW; // про tr1 :: shared_ptr

Схожі статті