Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ
Шрифт:
Из этого примера следует извлечь два урока. Первый – член данных инкапсулирован лишь настолько, насколько доступна функция, возвращающая ссылку на него. В данном случае хотя ulhc и lrhc объявлены закрытыми, но на самом деле они открыты, потому что на них возвращают ссылки открытые функции upperLeft и lowerRight. Второй урок в том, что если константная функция-член возвращает ссылку на данные, ассоциированные с объектом, но хранящиеся вне самого объекта, то код, вызывающий эту функцию, может модифицировать данные. (Все это последствия ограничений побитовой константности – см. правило 3.)
Такой результат получился, когда мы использовали функции-члены, возвращающие
Обычно, говоря о внутреннем устройстве объекта, мы имеем в виду его данные-члены, но функции-члены, к которым нет открытого доступа (то есть объявленные в секции private или protected), также являются частью внутреннего устройства. Поэтому возвращать их «дескрипторы» тоже не следует. Иными словами, нельзя, чтобы функция-член возвращала указатель на менее доступную функцию-член. В противном случае реальный уровень доступа будет определять более доступная функция, потому что клиенты смогут получить указатель на менее доступную функцию и вызвать ее через такой указатель.
Впрочем, функции, которые возвращают указатели на функции-члены, встречаются нечасто, поэтому вернемся к классу Rectangle и его функциям-членам upperLeft и lowerRight. Обе проблемы, которые мы идентифицировали для этих функций, могут быть исключены простым применением квалификатора const к их возвращаемому типу:
В результате такого изменения пользователи смогут читать объекты Point, определяющие прямоугольник, но не смогут изменять их. Это значит, что объявление константными функций upperLeft и lowerRight больше не является ложью, так как они более не позволяют клиентам модифицировать состояние объекта. Что касается проблемы инкапсуляции, то мы с самого начала намеревались дать клиентам возможность видеть объекты Point, определяющие Rectangle, поэтому в данном случае ослабление инкапсуляции намеренное. К тому же это лишь частичное ослабление: рассматриваемые функции дают только доступ для чтения. Доступ для записи по-прежнему запрещен.
Но даже и так upperLeft и lowerRight по-прежнему возвращают «дескрипторы» внутренних данных объекта, и это может вызвать проблемы иного свойства. В частности, возможно появление «висячих дескрипторов» (dangling handles), то есть дескрипторов, ссылающихся на части уже не существующих объектов. Наиболее типичный источник таких исчезнувших объектов – значения, возвращаемые функциями. Например, рассмотрим функцию, которая возвращает ограничивающий прямоугольник объекта GUI:
Теперь посмотрим, как пользователь может применить эту функцию:
Вызов boundingBox вернет новый временный объект Rectangle. Этот объект не имеет имени, поэтому назовем его temp. Затем вызывается функция-член upperLeft объекта temp, и этот вызов возвращает ссылку на внутренние данные temp, в данном случае на один из объектов Point. В результате pUpperLeft указывает на этот объект Point. До сих пор все шло хорошо, но мы еще не закончили, поскольку в конце предложения возвращенное boundingBox значение – temp – будет разрушено, а это приведет к разрушению объектов Point, принадлежавших temp. То есть pUpperLeft теперь указывает на объект, который более не существует. Указатель PUpperLeft становится «висячим» уже в конце предложения, где он создан!
Вот почему опасна любая функция, которая возвращает «дескриптор» внутренних данных объекта. При этом не важно, является ли «дескриптор» ссылкой, указателем или итератором. Не важно, что она квалифицирована const. Не важно, что сама функция-член, возвращающая «дескриптор», является константной. Имеет значение лишь тот факт, что «дескриптор» возвращен, поскольку возникает опасность, что он «переживет» объект, с которым связан.
Это не значит, что никогда не следует писать функции-члены, возвращающие дескрипторы. Иногда это бывает необходимо. Например, operator[] позволяет вам обращаться к отдельному элементу строки или вектора, и работает он, возвращая ссылку на данные в контейнере (см. правило 3), которые уничтожаются вместе с контейнером. Но все же такие функции – скорее исключение, чем правило.
• Избегайте возвращать «дескрипторы» (ссылки, указатели, итераторы) внутренних данных объекта. Это повышает степень инкапсуляции, помогает константным функциям-членам быть константными и минимизирует вероятность появления «висячих дескрипторов».
Правило 29: Стремитесь, чтобы программа была безопасна относительно исключений
Безопасность исключений в чем-то подобна беременности… но пока отложим эту мысль в сторонку. Нельзя всерьез говорить о репродуктивной функции, пока не завершился этап ухаживания.
Предположим, что у нас есть класс, представляющий меню с фоновыми картинками в графическом интерфейсе пользователя. Этот класс предназначен для использования в многопоточной среде, поэтому он включает мьютекс для синхронизации доступа: