43. Разумно пользуйтесь идиомой Pimpl
43. Разумно пользуйтесь идиомой Pimpl
Резюме
С++ делает закрытые члены недоступными, но не невидимыми. Там, где это оправдывается получаемыми преимуществами, следует подумать об истинной невидимости, достигаемой применением идиомы Pimpl (указателя на реализацию) для реализации брандмауэров компилятора и повышения сокрытия информации (см. рекомендации 11 и 41).
Обсуждение
Когда имеет смысл создать "брандмауэр компилятора", который полностью изолирует вызывающий код от закрытых частей класса, воспользуйтесь идиомой Pimpl (указателя на реализацию): скройте их за непрозрачным указателем (указатель (предпочтительно подходящий интеллектуальный) на объявленный, но пока не определенный класс). Например:
class Map {
// ...
private:
struct Impl;
shared_ptr<Impl> pimpl_;
};
Дающий название идиоме указатель должен использоваться для хранения всех закрытых членов, как данных, так и закрытых функций-членов. Это позволяет вам вносить произвольные изменения в закрытые детали реализации ваших классов без какой бы то ни было рекомпиляции вызывающего кода. Свобода и независимость — вот отличительные черты рассматриваемой идиомы (см. рекомендацию 41).
Примечание: объявляйте указатель на закрытую реализацию, как показано — с использованием двух объявлений. Если вы скомбинируете две строки с предварительным объявлением типа и указателя на него в одну инструкцию struct Impl *pimpl; это будет вполне законно, но изменит смысл объявления: в этом случае Impl находится в охватывающем пространстве имен и не является вложенным типом вашего класса.
Имеется как минимум три причины для использования Pimpl, и все они вытекают из различия между доступностью (в состоянии ли вы вызвать или использовать некоторый объект) и видимостью (видим ли этот объект для вас и, таким образом, зависите ли вы от его определения) в С++. В частности, все закрытые члены класса недоступны никому, кроме функций- членов и друзей, но зато видимы всем — любому коду, которому видимо определение класса.
Первое следствие этого — потенциально большее время сборки приложения из-за обработки излишних определений типов. Для закрытых данных-членов, хранящихся по значению, и параметров закрытых функций-членов, передаваемых по значению или используемых в видимой реализации функций, типы должны быть определены, даже если они никогда не потребуются в данной единице компиляции. Это может привести к увеличению времени сборки, например:
class C {
// ...
private:
AComplicatedType act_;
}
Заголовочный файл, содержащий определение класса С, должен также включать заголовочный файл, содержащий определение AComplicatedType, который в свою очередь транзитивно включает все заголовочные файлы, которые могут потребоваться для определения AComplicatedType, и т.д. Если заголовочные файлы имеют большие размеры, время компиляции может существенно увеличиться.
Второе следствие — создание неоднозначностей и сокрытие имен для кода, который пытается вызвать функцию. Несмотря на то, что закрытая функция-член не может быть вызвана кодом вне ее класса и его друзей, она тем не менее участвует в поиске имен и разрешении перегрузки и тем самым может сделать вызов неоднозначным или некорректным. Перед выполнением проверки доступности С++ выполняет поиск имен и разрешение перегрузки. Из-за этого видимость имеет более высокий приоритет:
int Twice(int); // 1
class Calc {
public:
string Twice(string); // 2
private:
char* Twice(char*); // 3
int Test() {
return Twice(21); // A: ошибка, функции 2 и 3 не
// подходят (могла бы подойти функция 1, но
// ее нельзя рассматривать, так она скрыта от
// данного кода)
}
};
Calc с;
с.Twice("Hello"); // Б: ошибка, функция 3
// недоступна (могла бы использоваться
// функция 2, но она не рассматривается, так
// как у функции 3 лучшее соответствие
// аргументу)
В строке А обходной путь состоит в том, чтобы явно квалифицировать вызов как ::Twice(21) для того, чтобы заставить поиск имен выбрать глобальную функцию. В строке Б обходной путь состоит в добавлении явного преобразования типа с.Twiсе(string("Hellо")) для того, чтобы заставить разрешение перегрузки выбрать соответствующую функцию. Некоторые из таких проблем, связанных с вызовами, можно решить и без применения идиомы Pimpl, например, никогда не используя закрытые перегрузки функций-членов, но не для всех проблем, разрешимых при помощи идиомы Pimpl, можно найти такие обходные пути.
Третье следствие влияет на обработку ошибок и безопасность. Рассмотрим пример Widget Тома Каргилла (Tom Cargill):
class Widget { // ...
public:
Widget& operator=(const Widget&);
private:
T1 t1_;
T2 t2_;
};
Коротко говоря, мы не можем написать оператор operator=, который обеспечивает строгую гарантию (или хотя бы базовую гарантию), если операции T1 или T2 могут давать необратимые сбои (см. рекомендацию 71). Хорошие новости, однако, состоят в том, что приведенная далее простая трансформация всегда обеспечивает, как минимум, базовую гарантию для безопасного присваивания, и как правило — строгую гарантию, если необходимые операции T1 и T2 (а именно — конструкторы и деструкторы) не имеют побочных эффектов. Для этого следует хранить объекты не по значению, а посредством указателей, предпочтительно спрятанными за единственным указателем на реализацию:
class Widget { // ...
public:
Widget& operator=(const Widget&);
private:
struct Impl;
shared_ptr<Impl> pimpl_;
};
Widget& Widget::operator=( const Widget& ) {
shared_ptr<Impl> temp(new Impl( /*...*/ ));
// изменяем temp->t1_ и temp->t2_; если какая-то из
// операций дает сбой, генерируем исключение, в
// противном случае - принимаем внесенные изменения:
pimpl_ = temp;
return *this;
}
Исключения
В то время как вы получаете все преимущества дополнительного уровня косвенности, проблема состоит только в увеличении сложности кода (см. рекомендации 6 и 8).
Ссылки
[Coplien92] §5.5 • [Dewhurst03] §8 • [Lakos96] §6.4.2 • [Meyers97] §34 • [Murray93] §3.3 • [Stroustrup94] §2.10, §24.4.2 • [Sutter00] §23, §26-30 • [Sutter02] §18, §22 • [Sutter04] §16-17
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОКЧитайте также
Не пользуйтесь прокруткой без необходимости!
Не пользуйтесь прокруткой без необходимости! Одна строка окна программного кода может содержать примерно 300 символов (наверное, вы удивитесь, если я скажу, что максимальное число символов равно 308), так что при желании можно вместить в одну строку довольно длинный
64. Разумно сочетайте статический и динамический полиморфизм
64. Разумно сочетайте статический и динамический полиморфизм РезюмеСтатический и динамический полиморфизм дополняют друг друга. Следует ясно представлять себе их преимущества и недостатки, чтобы использовать каждый из них там, где он дает наилучшие результаты, и
85. Пользуйтесь правильным алгоритмом поиска
85. Пользуйтесь правильным алгоритмом поиска РезюмеДанная рекомендация применима к поиску определенного значения в диапазоне. При поиске в неотсортированном диапазоне используйте алгоритмы find/find_if или count/count_if. Для поиска в отсортированном диапазоне выберите lower_bound,
86. Пользуйтесь правильным алгоритмом сортировки
86. Пользуйтесь правильным алгоритмом сортировки РезюмеПри сортировке вы должны четко понимать, как работает каждый из сортирующих алгоритмов, и использовать наиболее дешевый среди тех, которые пригодны для решения вашей задачи.ОбсуждениеВам не всегда требуется полный
Совет 12. Разумно оценивайте потоковую безопасность контейнеров STL
Совет 12. Разумно оценивайте потоковую безопасность контейнеров STL Мир стандартного С++ выглядит старомодным и не подверженным веяниям времени. В этом мире все исполняемые файлы компонуются статически, в нем нет ни файлов, отображаемых на память, ни общей памяти. В нем нет
Пользуйтесь обычными словами
Пользуйтесь обычными словами Вставьте настоящий текст вместо lorem ipsumСлова lorem ipsum dolor — признанные друзья дизайнеров. Текст-пустышка помогает понять, как будет выглядеть дизайн, когда он будет воплощен. Но текст-пустышка может быть опасным.Lorem ipsum меняет ваш взгляд на