Черная магия с использованием Initializer_list и расширения пакета

Для расширения параметров гибкой функции существует метод, использующий std::initializer_list. Однако я не мог этого понять. Может ли кто-нибудь объяснить это понятным образом?

template<typename T, typename... Args>
auto print(T value, Args... args) {
    std::cout << value << std::endl;
    return std::initializer_list<T>{([&] {
        std::cout << args << std::endl;
    }(), value)...};
}

1 ответ

Это очень запутанный способ делать что-то, но C++14 требует, чтобы мы делали нечто подобное. Я объясню ограничения и почему это делается таким образом (хотя есть более ясные способы сделать это).

Цель этого кода - многократно распечатать каждый параметр, указанный в отдельной строке. Поскольку функция является вариативным шаблоном, она должна использовать расширение пакета для выражения std::cout << args << std::endl.

Ваша первая мысль может быть (std::cout << args << std::endl) ...;. Однако на самом деле это недопустимая вещь, которую вы можете сделать в C++14. Фактически, вы можете выполнить расширение пакета только в контексте последовательности значений, разделенных запятыми, в C++14, например, списка аргументов функции или чего-то еще. Вы не можете просто расширить пакет как голое заявление.

Что ж, одно место, куда вы можете развернуть пакет, - это список инициализации в фигурных скобках ({}для инициализации объектов). Тем не мение, {(std::cout << args << std::endl) ...};тоже не работает. В расширении нет ничего плохого; проблема в самом списке инициализации в фигурных скобках. Грамматически список инициализации в фигурных скобках может появиться только при инициализации объекта. И голый {}поскольку оператор ничего не инициализирует. Так что вы не можете его там использовать.

Итак, вы должны использовать {}чтобы что-то инициализировать. Типичная идиома для этого - инициализировать пустой массив. Например:

intunused[] = {0, ((std::cout << args << std::endl), 0)...};

Начальный 0, необходимо в случае argsпусто; вы не можете инициализировать массив без размера без элементов. В конце , 0 в выражении раскрытия является частью выражения запятой.

В C++ выражение (1, 2)означает "оценить выражение 1, затем отбросить его значение, оценить выражение 2 и использовать его как результат общего выражения". Таким образом, использование этого в раскрытии пакета означает `вывести один аргумент, отбросить результат и использовать 0 в качестве результата выражения. Таким образом, каждое расширение пакета - это просто причудливый способ сказать "0" в том, что касается результата выражения.

В конце концов, unusedпросто хранит кучу нулей. Мы используем побочный эффект инициализации unused как способ заставить C++ распаковать расширение пакета.

В коде, который вы показываете, пользователь решил использовать список braced-init-list для непосредственной инициализации initializer_list<T>. Это тоже верно, и у него есть незначительное преимущество работы с пустым args. Проблема в том, что затем пользователь возвращает этот объект. Это плохо, потому что никто не может использовать это возвращаемое значение.

initializer_listне владеют объектами, на которые они ссылаются. Временный массив создается на месте списка фигурных скобок, который содержит эти объекты; то initializer_listпросто указывает на этот массив. Временный массив будет уничтожен в конце return оператор, поэтому вызывающий получит initializer_listэто указывает на кучу разрушенных объектов. Кроме того, он вызывает конструктор копирования T кучу раз напрасно, так как никто не может использовать возвращаемое значение.

Итак, это запутанный и плохой пример распространенной идиомы. Лучше бы убрать return и просто сделай это void функция.

C++ 17 просто позволяет нам делать это напрямую, без необходимости инициализировать массив:

((std::cout << args << std::endl), ...);

Это выражение сгиба над оператором запятой. Он будет вызывать каждое подвыражение первым до последнего для значений в args.

Другие вопросы по тегам