Черная магия с использованием 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
.