42. Не допускайте вмешательства во внутренние дела

42. Не допускайте вмешательства во внутренние дела

Резюме

Избегайте возврата дескрипторов внутренних данных, управляемых вашим классом, чтобы клиенты не могли неконтролируемо изменять состояние вашего объекта, как своего собственного.

Обсуждение

Рассмотрим следующий код:

class Socket {

public:

 // ... Конструктор, который открывает handle_,

 // деструктор, который закрывает handle_, и т.д. ...

 int GetHandle() const { return handle_; } // Плохо!

private:

 int handle_; // дескриптор операционной системы

};

Сокрытие данных — мощный инструмент абстракции и модульности (см. рекомендации 11 и 41). Однако сокрытие данных при одновременном обеспечении доступа к их дескрипторам обречено на провал, потому что это то же, что и закрыть свою квартиру на замок и положить ключ под коврик у входа или просто оставить его в замке. Вот почему это так.

• В этом случае клиент имеет две возможности реализации функциональности. Он может воспользоваться абстракцией вашего класса (Socket) либо непосредственно работать с реализацией, на которой основан ваш класс (дескриптор сокета в стиле С). В последнем случае объект оказывается не осведомлен об изменениях, происходящих с ресурсом, которым он, как ему кажется, владеет. Теперь класс не в состоянии надежно обогатить или усовершенствовать функциональность (например, обеспечить прокси, журнализацию, сбор статистики и т.п.), поскольку клиенты могут просто обойти эти возможности реализации, как и любые другие инварианты, которые вы, как вы полагаете, добавили в ваш класс. Это делает невозможной, в частности, корректную обработку возникающих ошибок (см. рекомендацию 70).

• Класс не может изменять внутреннюю реализацию своей абстракции, поскольку от нее зависят клиенты. Если в будущем класс Socket будет обновлен для поддержки другого протокола с использованием других низкоуровневых примитивов, вызывающий код, который будет по-прежнему получать доступ к дескриптору handle_ и работать с ним, окажется некорректным.

• Класс не в состоянии обеспечить выполнение его инвариантов, поскольку вызывающий код может изменить состояние без ведома класса. Например, кто-то может закрыть дескриптор, используемый объектом Socket, минуя вызов функции-члена Socket, а это приведет к тому, что объект станет недействительным.

• Код клиента может хранить дескрипторы, возвращаемые вашим классом, и пытаться использовать их после того, как код вашего класса сделает их недействительными.

Распространенная ошибка заключается в том, что действие const на самом деле неглубокое и не распространяется посредством указателей (см. рекомендацию 15). Например, Socket::GetHandle — константный член; пока мы рассматриваем ситуацию с точки зрения компилятора, возврат handlе_ сохраняет константность объекта. Однако непосредственный вызов функций операционной системы с использованием значения handlе_ вполне может изменять данные, к которым косвенно обращается handlе_.

Приведенный далее пример очень прост, хотя в данном случае ситуация несколько лучше — мы можем снизить вероятность случайного неверного употребления возвращаемого значения, описав его тип как const:

class String {

 char* buffer_;

public:

 char* GetBuffer() const { return buffer_; }

 // Плохо: следует возвращать const char*

 // ...

};

Хотя функция GetBuffer константная, технически этот код вполне корректен. Понятно, что клиент может использовать эту функцию GetBuffer для того, чтобы изменить объект String множеством разных способов, не прибегая к явному преобразованию типов. Например, strcpy(s.GetBuffer(), "Very Long String...") — вполне законный код; любой компилятор пропустит его без каких бы то ни было замечаний. Если бы мы объявили возвращаемый тип как const char*, то представленный код вызвал бы, по крайней мере, ошибку времени компиляции, так что случайно поступить столь опасно было бы просто невозможно — вызывающий код должен был бы использовать явное преобразование типов (см. рекомендации 92 и 95).

Но даже возврат указателей на const не устраняет возможности случайного некорректного использования, поскольку имеется еще одна проблема, связанная с корректностью внутренних данных класса. В приведенном выше примере с классом String, вызывающий код может сохранить значение, возвращаемое функцией GetBuffer, а затем выполнить операции, которые приведут к росту (и перемещению) буфера String, что в результате может привести к использованию сохраненного, но более недействительного указателя на несуществующий в данный момент буфер. Таким образом, если вы считаете, что у вас есть причины для обеспечения такого доступа ко внутреннему состоянию, вы должны детально документировать, как долго возвращаемое значение остается корректным и какие операции делают его недействительным (сравните с гарантиями корректности явных итераторов стандартной библиотеки; см. [C++03]).

Исключения

Иногда классы обязаны предоставить доступ ко внутренним дескрипторам по причинам, связанным с совместимостью, например, для интерфейса со старым кодом или при использовании других систем. Например, std::basic_string предоставляет доступ к своему внутреннему дескриптору посредством функций-членов data и c_str для совместимости с функциями, которые работают с указателями С — но не для того, чтобы хранить эти указатели и пытаться выполнять запись с их помощью! Такие функции доступа "через заднюю дверь" всегда являются злом и должны использоваться очень редко и очень осторожно, а условия корректности возвращаемых ими дескрипторов должны быть точно документированы.

Ссылки

[С++03] §23 • [Dewhurst03] §80 • [Meyers97] #29 • [Saks99] • [Stroustrup00] §7.3 • [Sutter02] §9