Выражение сгиба с оператором запятой и набором параметров вариабельного шаблона

#include<iostream>
using namespace std;

template<typename ...Args>
void output_argus(Args&&... args) 
{
    ((cout << args << '\n'), ...);    // #1
    (... , (cout << args << '\n'));   // #2
}


int main()
{
    output_argus(1, "test", 5.6f);
}

Основанный на c++ оператор doc, ',' является оператором слева направо. Это означает a, b, c, d означающее (((a, b), c),d) не (a, (b, (c, d))). Это важно, если a, b, c, d являются утверждениями.

Однако, основываясь на выражении doc для ',' которое должно использовать одинарную левую складку.

Мой вопрос, почему оба утверждения в моем коде работают? Разве не должен работать только № 2? А также как понять ... и args. а вложенные выражения складываются?

Ответ 1

Допустим, мы складываем 3 выражения над бинарным оператором с одинарным сгибом. У нас есть два варианта: (xs @...) (одинарный правый сгиб) и (... @xs) (одинарный левый сгиб).

(xs @...) расширяется до (a @(b @c))

(... @xs) расширяется до ((a @b) @c)

Что можно сказать о разнице между выражениями a @(b @c) и (a @b) @c? Если @ ассоциативно относится к этим типам, то эти два выражения идентичны. Вот что значит ассоциативный. Если у вас был набор параметров целых чисел, то унарный левый сгиб над + и унарный правый сгиб над + будут иметь одинаковое значение (переполнение по модулю), потому что сложение ассоциативно. Вычитание, с другой стороны, не ассоциативно. (xs -...) и (... - xs) означают совершенно разные вещи.

Аналогично, , оператор в C++ ассоциативно. Неважно, каким образом вы заключите в скобки выражения. ((a, b), c) и (a, (b, c)) оба оценивают и отбрасывают a, затем оценивают и отбрасывают b, затем оценивают c и тот результат. Проще увидеть, сводишь ли ты выражения к буквам, почему это так.

В результате оба ((cout << args << '\n'),...) и (..., (cout << args << '\n')) делают одно и то же, и оба они фактически означает:

cout << args1 << '\n';
cout << args2 << '\n';
// ...
cout << argsN << '\n';

Ответ 2

На странице со ссылкой ваш # 1 расширяется следующим образом:

((cout << args₀ << '\n'), ((cout << args₁ << '\n'), (cout << args₂ << '\n')));

Удаление повторения, чтобы сделать его чище:

args₀, (args₁, args₂)

Для # 2 расширение сводится к:

(args₀, args₁), args₂

Давайте пройдемся по оценке каждого из них.

# 1:

args₀  ,  (args₁, args₂)
      ^^^

Подчеркнутая запятая оценивает левую сторону, печатая 1. Затем оценивается правая сторона, которая оценивает печать args₁, печать test, затем печать args₂, печать 5.6.

# 2:

(args₀, args₁)  ,  args₂
               ^^^

Подчеркнутая запятая оценивает левую сторону. Это запускает оценку другой запятой, которая оценивает печать args₀, печать 1, затем оценивает печать args₁, test печати. Теперь подчеркнутая запятая завершена с оценкой левой стороны и оценкой правой стороны, печать 5.6.

Как видите, оба производят одинаковый порядок оценки каждого отдельного аргумента, несмотря на различие в скобках.

Обратите внимание, что в общем случае это не всегда так. Некоторые операторы, такие как +, не имеют гарантированного порядка вычисления, как оператор запятой. Если бы такой оператор использовался вместо запятой для объединения выражений печати, компилятор мог бы в конечном итоге выбрать вывод отдельных аргументов в любом порядке.