Это выглядит как дублирование кода, но на самом деле это всего лишь "дублирование сигнатур", поскольку все три варианта обычно используют одну и ту же функцию. Вряд ли вы впадете в ересь преждевременной оптимизации (см. рекомендацию 8) при такой простой перегрузке, тем более что этот метод слабо применим при проектировании
библиотек, когда трудно заранее предсказать, какие именно типы будут использоваться в коде, критическом по отношению к производительности.
Мудрость — это знание того, когда надо воздержаться. Встроенные операторы
&&
,
||
и
,
(запятая) трактуются компилятором специальным образом. После перегрузки они становятся обычными функциями с весьма отличной семантикой (при этом вы нарушаете рекомендации 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,
для последовательности инициализаций. Некоторая библиотека пытается упростить добавление нескольких значений в контейнер за один раз путем перегрузки оператора-запятой. Например, для добавления в