43. Разумно пользуйтесь идиомой Pimpl

We use cookies. Read the Privacy and Cookie Policy

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