Чтение онлайн

ЖАНРЫ

Стандарты программирования на С++. 101 правило и рекомендация

Александреску Андрей

Шрифт:

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*
; такое преобразование типов выполняются при помощи двух именованных функций —
c_str
и
data
.

• Все операторы сравнений, определенные для

std::string
(например,
==
,
!=
,
<
), перегружены для сравнения
const char*
и
std::string
в любом порядке (см. рекомендацию 29). Это позволяет избежать создания скрытых временных переменных.

Но и при этом возникают определенные неприятности, связанные с перегрузкой функций.

void Display(int);

void Display(std::string);

Display(NULL); // вызов Display(int)

Этот результат для некоторых может оказаться сюрпризом. (Кстати, если бы выполнялся вызов

Display(std::string)
, код бы обладал неопределенным поведением, поскольку создание
std::string
из нулевого указателя некорректно, но конструктор этого класса не обязан проверять передаваемое ему значение на равенство нулю.)

Ссылки

[Dewhurst03] §36-37 • [Lakos96] §9.3.1 • [Meyers96] §5 • [Murray93] §2.4 • [Sutter00] §6, §20, §39

41. Делайте данные-члены закрытыми (кроме случая агрегатов в стиле структур С)

Резюме

Данные-члены должны быть закрыты. Только в случае простейших типов в стиле структур языка С, объединяющих в единое целое набор значений, не претендующих на инкапсуляцию и не обеспечивающих поведение, делайте все данные-члены открытыми. Избегайте смешивания открытых и закрытых данных, что практически всегда говорит о бестолковом дизайне.

Обсуждение

Сокрытие информации является ключом к качественной разработке программного обеспечения (см. рекомендацию 11). Желательно делать все данные-члены закрытыми; закрытые данные — лучшее средство для сохранения инварианта класса, в том числе при возможных вносимых изменениях.

Открытые данные — плохая идея, если класс моделирует некоторую абстракцию и, следовательно, должен поддерживать инварианты. Наличие открытых данных означает, что часть состояния вашего класса может изменяться неконтролируемо, непредсказуемо и асинхронно с остальной частью состояния. Это означает, что абстракция разделяет ответственность за поддержание одного или нескольких инвариантов с неограниченным множеством кода, который использует эту абстракцию, и совершенно очевидно, что такое положение дел просто недопустимо с точки зрения корректного проектирования.

Поделиться с друзьями: