Это тот самый случай, когда с виду простой код заставляет разобраться во множестве тонкостей языка.
Для лучшего понимания проходящих в коде процессов сперва требуется внимательно присмотреться к стандарту языка.
Что стандарт говорит нам о
перегрузке операторов?
A declaration whose declarator-id is an operator-function-id shall declare a function or function template or an explicit instantiation or specialization of a function template. A function so declared is an operator function.
cout << a.get() << b.get();
Данный код маскирует два вызова одной функции -
std::ostream& operator << ( std::ostream&, int )
.
Тут очень важно уточнить форму перегрузки оператора, т.к. результат поведения этого кода сильно зависит от применяемой формы перегрузки.
Относительно стандартной формы перегрузки
operator << ()
стандарт
говорит что это перегрузка в форме внешней функции.
Значит приведенный код можно записать как:
operator<<( operator<<( cout, a.get() ), b.get() );
И именно с этого момента начинается самое интересное.
Что стандарт говорит нам о вызове функций? А говорит он совсем разные вещи.
C++14 [expr.call#5.2.2.8] заявляет, что:
The evaluations of the postfix expression and of the arguments are all unsequenced relative to one another. All side effects of argument evaluations are sequenced before the function is entered (see 1.9).
C++17 [expr.call#8.2.2.5] утверждает, что:
If an operator function is invoked using operator notation, argument evaluation is sequenced as specified for the built-in operator; see 16.3.1.2.
В результате, если транслировать данный код как код 14-го (или старших) стандарта, поведение у этого кода будет одно. Если же код транслировать как код 17-го (и моложе) стандарта, его поведение будет будет уже другим.
А что же там с вероятным неопределенным поведением? Ведь неупорядоченная модификация состояния является UB. И, вроде как,
cout << a.get() << b.get();
можно упростить до
cout << ++i << ++i;
, что уже более явно должно показывать наличие UB.
UB в этом коде нет. И вот почему.
Для определения порядка вычисления участков выражения следует руководствоваться
правилами упорядочивания выражений.
Среди прочих правил там записаны важные для нас сейчас. Я приведу цитаты.
2) The value computations (but not the side-effects) of the operands to any operator are sequenced before the value computation of the result of the operator (but not its side-effects).
3) When calling a function (whether or not the function is inline, and whether or not explicit function call syntax is used), every value computation and side effect associated with any argument expression, or with the postfix expression designating the called function, is sequenced before execution of every expression or statement in the body of the called function.
5) The side effect of the built-in pre-increment and pre-decrement operators is sequenced before its value computation (implicit rule due to definition as compound assignment)
16) Every overloaded operator obeys the sequencing rules of the built-in operator it overloads when called using operator notation. (since C++17)
19) In a shift operator expression E1<<E2
and E1>>E2
, every value computation and side-effect of E1 is sequenced before every value computation and side effect of E2. (since C++17)
До C++17 порядок вычисления операндов
cout << a.get() << b.get();
не определен, но поведение этого кода определено. Поэтому при трансляции по стандарту C++14 этот код может выдать или
12
, или
21
. Но не
11
.
Начиная с C++17 порядок вычисления операндов строго определен и является интуитивным, а результат выполнения
cout << a.get() << b.get();
всегда однозначен. При трансляции этого кода по стандарту C++17 (и дальше) в консоль будет выведено всегда и только
12
.
До C++11 поведение кода
cout << a.get() << b.get();
не определено.
Сегодня мы уже не задумываемся о жизни до стандарта C++11, поэтому я не скажу что в общем смысле в этом коде присутствует UB. Я скажу что UB тут нет. Но тем не менее, я бы рекомендовал избегать присутствия подобного кода в проектах даже если используется стандарт C++17 и дальше.