необходимости перекрывать их все (точнее говоря, данный пример можно рассматривать как применение шаблона проектирования Template Method).

• Управление базовым классом. Теперь базовый класс находится под полным контролем своего интерфейса и стратегии и может обеспечить пост- и предусловия интерфейса (см. рекомендации 14 и 68), причем выполнить всю эту работу в одном удобном повторно используемом месте — невиртуальной функции интерфейса. Такое 'предварительное разложение' обеспечивает лучший дизайн класса.

• Базовый класс более устойчив к изменениям. Мы можем позже изменить наше мнение и добавить некоторую проверку пост- или предусловий, или разделить выполнение работы на большее количество шагов или переделать ее, реализовать более полное разделение интерфейса и реализации с использованием идиомы Pimpl (см. рекомендацию 43), или внести иные изменения в базовый класс, и все это никак не повлияет на код, использующий данный класс или наследующий его. Заметим, что гораздо проще начать работу с использования NVI (даже если открытые функции представляют собой однострочные вызовы соответствующих виртуальных функций), а уже позже добавлять все проверки и инструментальные средства, поскольку эта работа никак не повлияет на код, использующий или наследующий данный класс. Ситуация окажется существенно сложнее, если начать с открытых виртуальных функций и позже изменять их, что неизбежно приведет к изменениям либо в коде, который использует данный класс, либо в наследующем его.

См. также рекомендацию 54.

Исключения

NVI не применим к деструкторам в связи со специальным порядком их выполнения (см. рекомендацию 50).

NVI непосредственно не поддерживает ковариантные возвращаемые типы. Если вам требуется ковариантность, видимая вызывающему коду без использования dynamic_cast (см. также рекомендацию 93), проще сделать виртуальную функцию открытой.

Ссылки

[Allison98] §10 • [Dewhurst03] §72 • [Gamma95] • [Keffer95 pp. 6-7] • [Koenig97] §11 • [Sutter00] §19, §23 • [Sutter04] §18

40. Избегайте возможностей неявного преобразования типов

Резюме

Не все изменения прогрессивны: неявные преобразования зачастую приносят больше вреда, чем пользы. Дважды подумайте перед тем, как предоставить возможность неявного преобразования к типу и из типа, который вы определяете, и предпочитайте полагаться на явные преобразования (используйте конструкторы, объявленные как explicit, и именованные функции преобразования типов).

Обсуждение

Неявные преобразования типов имеют две основные проблемы.

• Они могут проявиться в самых неожиданных местах.

• Они не всегда хорошо согласуются с остальными частями языка программирования.

Неявно преобразующие конструкторы (конструкторы, которые могут быть вызваны с одним аргументом и не объявлены как explicit) плохо взаимодействуют с перегрузкой и приводят к созданию невидимых временных объектов. Преобразования типов, определенные как функции-члены вида operator T (где T — тип), ничуть не лучше — они плохо взаимодействуют с неявными конструкторами и позволяют без ошибок скомпилировать разнообразные бессмысленные фрагменты кода (примеров чего несть числа — см. приведенные в конце рекомендации ссылки; мы приведем здесь только пару из них).

В С++ последовательность преобразований типов может включать не более одного пользовательского преобразования. Однако когда в эту последовательность добавляются встроенные преобразования, ситуация может оказаться предельно запутанной. Решение здесь простое и состоит в следующем.

• По умолчанию используйте explicit в конструкторах с одним аргументом (см. рекомендацию 54):

class Widget { // ...

 explicit Widget(unsigned int widgetizationFactor);

 explicit Widget(const char* name, const Widget* other = 0);

};

• Используйте для преобразований типов именованные функции, а не соответствующие операторы:

class String { // ...

 const char* as_char_pointer() const; // в традициях c_str

};

См. также обсуждение копирующих конструкторов, объявленных как explicit, в рекомендации 54.

Примеры

Пример 1. Перегрузка. Пусть у нас есть, например, Widget::Widget (unsigned int), который может быть вызван неявно, и функция Display, перегруженная для Widget и double. Рассмотрим следующий сюрприз при разрешении перегрузки:

void Display(double);        // вывод double

void Display(const Widget&); // Вывод Widget

Display(5);                  // гм! Создание и вывод Widget

Пример 2. Работающие ошибки. Допустим, вы снабдили класс String оператором operator const char*:

class String {

 // ...

public:

 operator const char*(); // Грустное решение...

};

В результате этого становятся компилируемыми масса глупостей и опечаток. Пусть s1 и s2 — объекты типа String. Все приведенные ниже строки компилируются:

int x = s1 - s2;        // Неопределенное поведение

const char* р = s1 - 5; // Неопределенное поведение

р = s1 + '0';           // делает не то, что вы ожидаете

if (s1 == '0') { ... }  // делает не то, что вы ожидаете

Именно по этой причине в стандартном классе string отсутствует operator const char*.

Исключения

При нечастом и осторожном использовании неявные преобразования типов могут сделать код более коротким и интуитивно более понятным. Стандартный класс std::string определяет неявный конструктор, который получает один аргумент типа const char*. Такое решение отлично работает, поскольку проектировщики класса приняли определенные меры предосторожности.

• Не имеется автоматического преобразования std::string в const char*; такое преобразование типов выполняются при помощи двух именованных функций —

Добавить отзыв
ВСЕ ОТЗЫВЫ О КНИГЕ В ИЗБРАННОЕ

0

Вы можете отметить интересные вам фрагменты текста, которые будут доступны по уникальной ссылке в адресной строке браузера.

Отметить Добавить цитату