Мудрость — это знание того, когда надо воздержаться. Встроенные операторы &&
, ||
и ,
(запятая) трактуются компилятором специальным образом. После перегрузки они становятся обычными функциями с весьма отличной семантикой (при этом вы нарушаете рекомендации 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
vector<string> letters
:
set_cont(letters) += 'a', 'b';
Все в порядке, пока в один прекрасный день пользователь не напишет:
set_cont(letters) += getstr(), getstr();
// порядок не определен при использовании
// перегруженного оператора ','
Если функция getstr
получает, например, ввод пользователя и он введет строки 'с'
и 'd'
в указанном порядке, то в действительности строки могут оказаться внесены в любом порядке. Это может оказаться сюрпризом, поскольку при использовании встроенного оператора operator,
такой проблемы не возникает:
string s; s = getstr(), getstr(); // порядок строго определен
// при использовании
// встроенного оператора ','
Исключение — специализированные библиотеки шаблонов для научных вычислений, которые в соответствии с дизайном переопределяют все операторы.
31. Не пишите код, который зависит от порядка вычислений аргументов функции
Порядок вычисления аргументов функции не определен, поэтому никогда не полагайтесь на то, что аргументы будут вычисляться в той или иной очередности.
На начальных этапах развития языка C регистры процессора были драгоценным ресурсом и компиляторы решали трудные задачи эффективного их использования в сложных выражениях высокоуровневых языков. Для того чтобы позволить компилятору генерировать более быстрый код, создатели C дали распределителю регистров дополнительную степень свободы. При вызове функции порядок вычисления ее аргументов оставался неопределенным. Эта аргументация, вероятно, существенно менее важна в настоящее время, но главное, что порядок вычисления аргументов функций в C++ не