Данная статья была написана в ответ на многочисленные обсуждения в форумах о полезности, целесообразности и необходимости использования ключевого слова const в языке С++. Именно поэтому она носит несколько эмоциональный характер и содержит только основные ударные точки, а не системное изложение проблемы.
Ответ на этот вопрос прост для профессионалов: использование ключевого слова const позволяет создавать интерфейсы с необходимыми гарантиями. Для профессионалов этот ответ достаточен, потому, что они способны понять, как много очень похожих вещей кроется за ним.
Для тех, кто хочет понять – попробую объяснить. Но прежде чем показывать фрагменты кода и пояснять, как это работает в каждом конкретном случае, я поясню причину появления ключевого слова const в языке C++. Изначально в языке C не было такого ключевого слова.
Своим появлением оно обязано языку С++, который ввёл такое понятие, как «сокрытие информации на уровне объектов». В чистом С тоже было сокрытие информации – только на уровне модулей. А вот C++ дал нам protected и private. В результате у объектов получилась «скрытая часть». Та, доступ к которой разрешён только самому объекту.
И всё было прекрасно и замечательно. И Гради Буч – гений своего времени. И Страуструп – молодец. Но, как говорится, “Welcome to the real world”. Объектно-ориентированная парадигма хороша для верхнего уровня системы. А на нижнем уровне требуется производительность высокочастотных взаимодействий. И очень часто хочется иметь (вопреки рекомендациям ООП) прямой доступ к содержимому объекта.
Да, это плохо. Да – нельзя. Но порой по-другому – невозможно. Причин тому много: производительность, совместимость со старыми библиотеками. И тогда мы идём на компромисс: мы хотим разрешить пользователю добраться до внутренних данных, но по возможности сохранить при этом инкапсуляцию. Хоть в каком-то виде. И классическое решение здесь – Read-Only. Игра в одни ворота. Да, клиент теперь зависит от реализации сервера. Половина преимуществ инкапсуляции – под ударом. Ещё не на помойке – я могу потом пояснить желающим почему – но под ударом. Но зато работает вторая половина – клиент не может без предупреждения менять внутренние данные сервера.
Обратимся к примерам.
Пример 1
Классика жанра:
const char * std::string::c_str() const
И это самое больное место! И это – та самая оптимизация, о которой, чтобы не портить хорошей статьи (http://www.gotw.ca/gotw/081.htm), умалчивает Саттер. Потому, что он может вывернуться – он может сказать, что тут нет никакой оптимизации – какая разница вернёт компилятор const char * или char *. Но смотреть надо не на результат – уже имеющийся код, а на более принципиальный вопрос – в рамках синтаксиса C++ эту задачу не решить иначе!!! Нет средств, чтобы выразить тоже самое компилятору! Можно обойтись без const, но это уже будет другой string и цена c_str будет намного выше. Оптимизация структуры данных и алгоритма здесь происходит, а не пары пересылаемых байт.
Возвращаемый тип утверждает, что клиент не имеет права менять полученную строку. И без const это пояснить невозможно!
Пример 2
Язык С++ дал прекрасное синтаксическое средство: передачу аргументов по ссылке. Вот только средство это без правильного использования совместно с ним ключевого слова const – очень опасное.
Я, как клиент, используя функцию, объявленную как
void foo(T& arg);
ничего не могу сказать о том, собирается ли сервер менять arg во время работы.
Опасна именно форма записи! В небольшой программе, которую пишет один человек, это может и не быть проблемой. В реальном большом проекте, где работают десятки человек, где клиенты и сервера пишутся разными людьми и меняются независимо и неожиданно – ключевое слово const приобретает роль важнейшего инструмента при построении интерфейса. Оно определяет контракт – сервер обязуется сейчас и впредь не менять переданное ему значение. Без этого ключевого клиент ни в чём не может быть уверен. И снова привет Саттеру. Снова оптимизация, но опять уровнем выше – не байты оптимизируются! Всё намного лучше - клиент теперь не должен сохранять предварительно копию аргумента «на всякий случай»!!! Мало?
Правда, у этого примера есть одно тонкое место: по коду клиента всё равно не понятно, будет ли меняться передаваемый параметр. Т.е. контракт есть, но его надо знать, а в месте вызова простым чтением это не заметно. Поэтому у нас сразу было введено правило: не использовать передачу параметров по ссылке для изменения передаваемых значений. Хочешь менять – проси явно указатель и передавай адрес. Сразу по тексту будет ясно, что аргумент планируют менять. Но в любом случае за соблюдением этого «добровольного соглашения» зорко следит const.
Пример 3
Ну и в довесок (хотя песня уже пошла про одно и то же) – константный метод в интерфейсе (базовом классе) гарантирует мне, что ни один кудесник не станет менять состояние объекта во время вызова данного метода. Отпадают все беседы вида: «а если объект переместится?», «а если удалится?». Сказано Render() const. Значит, всё – я могу быть спокоен, и не буду анализировать при построении архитектуры заведомо невозможные ходы. Ещё раз повторюсь – вопрос не о коде. Вопрос о принятии высокоуровневых решений, решении проблем, которые могли бы возникнуть, если бы не было этого контракта. С его использованием я ТОЧНО знаю, что их не будет.
В завершение
Ещё несколько слов о том, почему «никуда не деться», если не убедили примеры. Ключевым словом const можно попробовать не пользоваться до одной поры: пока кто-то в команде или поставщике библиотек не начал этого делать. Появление слова const в одной части системы (особенно, если это библиотека низкого уровня) приведёт к необходимости привести в порядок весь остальной код. Просто потому, что в противном случае объём const_cast'ов в программе превысит объём остального кода. Так что подумайте – готовы ли вы «перепахивать» интерфейсы своей системы, когда захотите интегрировать в неё стороннюю библиотеку, написанную с использованием const?
Теперь немного о паре наиболее часто звучащих контраргументов:
Аргумент 1: А вот возьмёт программист и напишет const_cast<T*>(myConstPointer).
Ответ 1: Пояснить, что такого делать нельзя. Что это – нарушение контракта. Что вообще компилятор имеет право положить такой объект (const) в константную память и выбросить исключение с аварийным завершением программы при обращении к нему на запись. Проследить, что понял. Оштрафовать, если повторится. Уволить, если не поймёт.
Аргумент 2: А я могу в комментариях написать, что не буду менять передаваемое мне значение!
Ответ 2: «Мы верим, что станешь программистом. Уволься сразу!». Никто никогда не выполнит работу компилятора лучше его. Тем более в большом проекте. И уж если кому-то проще написать «// Аргумент менять не буду. Зуб даю!» вместо “const” – это не лечится...