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

ЖАНРЫ

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

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

Шрифт:

class String { // ...

 String(const char* text); // Обеспечивает неявное

// преобразование типов

};

bool operator==(const String&, const String&);

// ... где-то в коде ...

if (someString == "Hello"){ ... }

Ознакомившись с приведенными выше определениями, компилятор компилирует приведенное сравнение таким образом, как если бы оно было записано в виде

someString == String("Hellо")
.
Это слишком расточительно — копировать символы, чтобы потом просто прочесть их. Решение этой проблемы очень простое — определить перегруженные функции, чтобы избежать преобразования типов, например:

bool operator==(const String& lhs, const string& rhs); // #1

bool operator==(const String& lhs, const char* rhs); // #2

bool operator==(const char* lhs, const String& rhs); // #3

Это выглядит как дублирование кода, но на самом деле это всего лишь "дублирование сигнатур", поскольку все три варианта обычно используют одну и ту же функцию. Вряд ли вы впадете в ересь преждевременной оптимизации (см. рекомендацию 8) при такой простой перегрузке, тем более что этот метод слабо применим при проектировании библиотек, когда трудно заранее предсказать, какие именно типы будут использоваться в коде, критическом по отношению к производительности.

Ссылки

[Meyers96] §21 • [Stroustrup00] §11.4, §C.6 • [Sutter00] §6

30. Избегайте перегрузки

&&
,
||
и
,
(запятой)

Резюме

Мудрость — это знание того, когда надо воздержаться. Встроенные операторы

&&
,
||
и
,
(запятая) трактуются компилятором специальным образом. После перегрузки они становятся обычными функциями с весьма отличной семантикой (при этом вы нарушаете рекомендации 26 и 31), а это прямой путь к трудноопределимым ошибкам и ненадежности. Не перегружайте эти операторы без крайней необходимости и глубокого понимания.

Обсуждение

Главная причина отказа от перегрузки операторов

operator&&
,
operator||
и
operator,
(запятая) состоит в том, что вы не имеете возможности реализовать полную семантику встроенных операторов в этих трех случаях, а программисты обычно ожидают ее выполнения. В частности, встроенные версии выполняют вычисление слева направо, а для операторов
&&
и
||
используются сокращенные вычисления.

Встроенные версии

&&
и
||
сначала вычисляют левую часть выражения, и если она полностью определяет результат (
false
для
&&
,
true
для
||
), то вычислять правое выражение незачем — и оно гарантированно не будет вычисляться. Таким образом мы используем эту возможность, позволяя корректности правого выражения зависеть от успешного вычисления левого:

Employee* е = TryToGetEmployee;

if (е && e->Manager)

 // ...

Корректность этого кода обусловлена тем, что

e->Manager
не будет вычисляться, если e имеет нулевое значение. Это совершенно
обычно и корректно — до тех пор, пока не используется перегруженный оператор
operator&&
, поскольку в таком случае выражение, включающее
&&
, будет следовать правилам функции:

• вызовы функций всегда вычисляют все аргументы до выполнения кода функции;

• порядок вычисления аргументов функций не определен (см. также рекомендацию 31). Давайте рассмотрим модернизированную версию приведенного ранее фрагмента, которая

использует интеллектуальные указатели:

some_smart_ptr<Employee> е = TryToGetEmployee;

if (е && e->Manager)

 // ...

Пусть в этом коде используется перегруженный оператор

operator&&
(предоставленный автором
some_smart_ptr
или
Employee
). Тогда мы получаем код, который для читателя выглядит совершенно корректно, но потенциально может вызвать
e->Manager
при нулевом значении
e
.

Некоторый иной код может не привести к аварийному завершению программы, но стать некорректным по другой причине — из-за зависимости от порядка вычислений двух выражений. Результат может оказаться плачевным. Например:

if (DisplayPrompt && GetLine) // ...

Если оператор

operator&&
переопределен пользователем, то неизвестно, какая из функций —
DisplayPrompt
или
GetLine
— будет вызвана первой. Программа в результате может ожидать ввода пользователя до того, как будет выведено соответствующее поясняющее приглашение.

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

Та же ненадежность наблюдается и в случае оператора-запятой. Так же, как и операторы

&&
и
||
, встроенный оператор-запятая гарантирует, что выражения будут вычислены слева направо (в отличие от
&&
и
||
, здесь всегда вычисляются оба выражения). Пользовательский оператор-запятая не может гарантировать вычислений слева направо, что обычно приводит к удивительным результатам. Например, если в следующем коде используется пользовательский оператор-запятая, то неизвестно, получит ли функция
g
аргумент 0 или 1:

int i = 0;

f(i++), g(i); //См. также рекомендацию 31

Примеры

Пример. Инициализация библиотеки при помощи перегруженного оператора

operator,
для последовательности инициализаций. Некоторая библиотека пытается упростить добавление нескольких значений в контейнер за один раз путем перегрузки оператора-запятой. Например, для добавления в
vector<string> letters
:

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