Обсуждение
Главная причина отказа от перегрузки операторов operator&&
, operator||
и operator,
(запятая) состоит в том, что вы не имеете возможности реализовать полную семантику встроенных операторов в этих трех случаях, а программисты обычно ожидают ее выполнения. В частности, встроенные версии выполняют вычисление слева направо, а для операторов &&
и ||
используются сокращенные вычисления.
Встроенные версии &&
и ||
сначала вычисляют левую часть выражения, и если она полностью определяет результат ( false
для &&
, true
для ||
), то вычислять правое выражение незачем — и оно гарантированно не будет вычисляться. Таким образом мы используем эту возможность, позволяя корректности правого выражения зависеть от успешного вычисления левого:
Employee* е = TryToGetEmployee();
if (е && e->Manager())
// ...
Корректность этого кода обусловлена тем, что e->Manager()
не будет вычисляться, если e имеет нулевое значение. Это совершенно обычно и корректно — до тех пор, пока не используется перегруженный оператор operator&&
, поскольку в таком случае выражение, включающее &&
, будет следовать правилам функции:
• вызовы функций всегда вычисляют все аргументы до выполнения кода функции;
• порядок вычисления аргументов функций не определен (см. также рекомендацию 31). Давайте рассмотрим модернизированную версию приведенного ранее фрагмента, которая
использует интеллектуальные указатели:
some_smart_ptr е = 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 letters
:
set_cont(letters) += "a", "b";
Все в порядке, пока в один прекрасный день пользователь не напишет:
set_cont(letters) += getstr(), getstr();
// порядок не определен при использовании
// перегруженного оператора ","
Если функция getstr
получает, например, ввод пользователя и он введет строки "с"
и "d"
в указанном порядке, то в действительности строки могут оказаться внесены в любом порядке. Это может оказаться сюрпризом, поскольку при использовании встроенного оператора operator,
такой проблемы не возникает:
Читать дальше