Правило 3 Скрізь, де тільки можна використовувати const

Чудова властивість модифікатора const полягає в тому, що він накладає певне семантичне обмеження: даний об'єкт не повинен модифікуватися, і компілятор буде проводити це обмеження в життя. const дозволяє вказати компілятор і програмістам, що певна величина повинна залишатися незмінною. У всіх подібних випадках ви повинні позначити це явно, закликаючи на допомогу компілятор і гарантуючи тим самим, що обмеження не буде порушено. Ключове слово const напрочуд багатостороннє. Поза класами ви можете використовувати його для визначення констант у глобальній області або просторі імен (див. правило 2), а також для статичних об'єктів (всередині файлу, функції або блоку). Усередині класів можна застосовувати його як для статичних, так і для нестатичних даних-членів. Для покажчиків можна специфікувати, чи має бути константним сам покажчик, дані, на які він вказує, або й те, й інше (або ні те, ні інше):

Цей синтаксис не такий страшний, як може здатися. Якщо слово const з'являється зліва від зірочки, константним є те, на що вказує покажчик; якщо справа, то сам покажчик є константним. Нарешті, якщо слово const з'являється з обох сторін, то константно і те, й інше. Коли те, що вказується, – константа, деякі програмісти ставлять const перед ідентифікатором типу. Інші – після ідентифікатора типу, але перед зірочкою. Семантичної різниці тут немає, тому такі функції приймають параметр одного й того самого типу:

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

Такий код був би абсолютно некоректним, якби a та b мали вбудований тип. Одним з критеріїв якості типів користувача є сумісність із вбудованими (див. також правило 18), а можливість присвоювання значення результату твору двох об'єктів представляється мені дуже далекою від сумісності. Якщо ж оголосити, що operator* повертає константне значення, така ситуація стане неможливою. Ось чому так слід робити. Щодо аргументів з модифікатором const важко сказати щось нове; вони ведуть себе як локальні константні const-об'єкти. Усюди, де це можливо, додайте цей модифікатор. Якщо модифікувати аргумент або локальний об'єкт не потрібно, оголосіть його як const. Вам всього-то доведеться набрати шість символів, зате це запобігатиме прикрих помилок типу «хотів надрукувати ==, а ненароком надрукував =» (до чого це призводить, ми щойно бачили).

Константні функції-члени [ред.]

Функцію operator[] у класі TextBlock можна використовувати так:

До речі, константні об'єкти найчастіше зустрічаються у реальних програмах у результаті передачі за вказівником чи посилання константу. Наведений вище приклад CTB є досить штучним. Але ось вам більш реалістичний:

Перевантажуючи operator[] і створюючи різні версії з різними типами, що повертаються, ви можете по-різному обробляти константні і неконстантні об'єкти TextBlock:

Це пояснюється тим, що значення вбудованого типу, що повертається функцією, модифікувати некоректно. Навіть якби це було припустимо, той факт, що C++ повертає об'єкти за значенням (див. правило 20), означав би таке: модифікувалася копіяtb.text[0], а чи не саме значення tb.text[0]. Навряд чи це те, на що ви очікуєте. Давайте трохи перепочинемо і пофілософствуємо. Що означає для функції члена бути константною? Існує два поширених поняття: побітова константність (також відома як фізична константність) і логічна константність. Прибічники побітової константності вважають, що функція-член константна тоді і лише тоді, коли вона не модифікує жодні дані-члени об'єкта (за винятком статичних), тобто не модифікує жодного біта всередині об'єкта. Визначення побітової константності добре тим, що її порушення легко виявити: компілятор просто шукає присвоєння членам класу. Фактично, побітова константність – це константність, визначена в C++: функція-член із модифікатором const не може модифікувати нестатичні дані-члени об'єкта, для якого вона викликана. На жаль, багато функцій-членів, які поводяться далеко не константно, проходять побітовий тест. Зокрема, функція-член, яка модифікує те, на що вказує покажчик, часто не веде себе як константна. Але якщо об'єкту належить лише покажчик, то функція формально є побітово константною, і компілятор не заперечуватиме. Це може призвести до несподіваної поведінки. Наприклад, припустимо, що є клас подібний до Text-Block, де дані зберігаються в рядках типу char * замість string, оскільки це необхідно для передачі в функції, написані мовою C, який не розуміє, що таке об'єкти типу string.

У цьому класі функцію operator[] (неправильно!) оголошено як константну функцію-член, хоча вона повертає посилання на внутрішні дані об'єкта (ця тема обговорюється в правилі 28). Залишимо це поки що осторонь і зазначимо, що реалізація operator[] ніяк немодифікує pText У результаті компілятор спокійно згенерує код функції operator[]. Адже вона справді є побітово константною, а це все, що компілятор може перевірити. Але подивіться, що відбувається:

Безперечно, є щось некоректне в тому, що ви створюєте константний об'єкт з певним значенням, викликаєте для нього лише константну функцію-член, проте змінюєте його значення! Це призводить до поняття логічної константності. Прихильники цієї філософії стверджують, що функції-члени з const можуть модифікувати деякі біти об'єкта, що викликав їх, але тільки так, щоб користувач не міг цього виявити. Наприклад, ваш клас CTextBlock міг би кешувати довжину текстового блоку при кожному запиті:

Ця реалізація length(), звичайно, не є побітово константною, оскільки може модифікувати значення членів textLength і lengthlsValid. Але в той же час з боку здається, що константність об'єктів CTextBlock не загрожує. Проте компілятор не згоден. Він наполягає на побітовій константності. Що робити? Рішення просте: використовуйте модифікатор mutable. Він звільняє нестатичні дані-члени від обмежень побітової константності:

Як уникнути дублювання в константних та неконстантних функціях-членах [ред.

Використання mutable – чудове вирішення проблеми, коли побітова константність вас не цілком влаштовує, але воно не усуває всіх труднощів, пов'язаних із const. Наприклад, уявіть, що operator[] у класі TextBlock (і CTextBlock) не лише повертає посилання на відповідний символ, але також перевіряє вихід за межі масиву, протоколює інформацію про доступ та, можливо, навіть перевіряє цілісність даних. Приміщення всієї цієї логіки в обидві версії функції operatorконстантну і неконстантну (навіть якщо забути, що тепер ми маємо надзвичайно довгі вбудовані функції - див. правило 30) - призводить до такого незграбного коду:

Ох! Наявні всі неприємності, пов'язані з дублюванням коду: збільшення часу компіляції, розміру програми та незручність супроводу. Звичайно, можна перемістити весь код для перевірки виходу за межі масиву та іншого в окрему функцію-член (природно, закриту), яку викликатимуть обидві версії operator[], але звернення до цієї функції все ж таки дублюватимуться. Насправді бажано було б реалізувати функціональність operator[] один раз, а використовувати у двох місцях. Тобто одна версія operator[] має викликати іншу. І це підводить нас до питання про відкидання константності. Із самого початку відзначимо, відкидати константність недобре. Я присвятив ціле правило 27 тому, щоб переконати вас не робити цього, але дублювання коду теж не цукор. У разі константна версія operator[] робить у точності те саме, що неконстантна, і відмінність з-поміж них – лише у присутності модифікатора const. У цій ситуації відкидати const безпечно, оскільки користувач, що викликає неконстантний operator[], так чи інакше має отримати неконстантний об'єкт. Адже в іншому випадку він не викликав би неконстантну функцію. Тому реалізація неконстантного operator[] шляхом виклику константної версії – це безпечний спосіб уникнути дублювання коду, навіть навіть для цього потрібно скористатися оператором const_cast. Нижче наведений код, що отримується в результаті, але він стане ясніше після того, як ви прочитаєте наступні пояснення: