С++ 17 Шаблоны шаблонов Variadic
Я не понимаю, почему это не работает. Может ли кто-то, кто разбирается в шаблонах и сворачивании вариантов, объясняет, что происходит и дает решение, которое действительно работает?
#include <iostream>
#include <string>
template <typename... Args>
void print(Args... args)
{
std::string sep = " ";
std::string end = "\n";
(std::cout << ... << sep << args) << end;
}
int main()
{
print(1, 2, 3);
}
Он должен распечатать каждый из аргументов с пробелом между ними и новой строкой в конце. Он работает, если вы удалите sep <<
, но тогда нет места между каждым аргументом при его печати.
Ответы
Ответ 1
Грамматика для двоичных fold-expressions должна быть одной из следующих:
(pack op ... op init)
(init op ... op pack)
У вас есть (std::cout << ... << sep << args)
, который не соответствует любой форме. Вам нужно что-то вроде (cout << ... << pack)
, поэтому удаляется sep
.
Вместо этого вы можете либо сбрасывать запятую:
((std::cout << sep << args), ...);
или используйте рекурсию:
template <class A, class... Args>
void print(A arg, Args... args) {
std::cout << arg;
if constexpr (sizeof...(Args) > 0) {
std::cout << sep;
print(args...);
}
}
Ответ 2
Это будет работать, но оно напечатает конечное пространство:
template <typename... Args>
void print(Args... args)
{
std::string sep = " ";
std::string end = "\n";
((std::cout << args << sep), ...) << end;
}
пример live wandbox
В этом случае выполняется свертка над оператором запятой, что приводит к расширению, подобному:
// (pseudocode)
(std::cout << args<0> << sep),
(std::cout << args<1> << sep),
(std::cout << args<2> << sep),
...,
(std::cout << args<N> << sep),
Ответ 3
Что вы действительно хотите сделать:
std::string sep = " ";
std::string end = "\n";
(std::cout << ... << (sep << args)) << end;
потому что вы хотите, чтобы (sep << args)
был сложен с помощью std::cout
. Это не работает, потому что sep << args
не знает, что он передается на std::cout
или передается вообще; <<
работает только потоком, если левая сторона является потоком.
Короче говоря, проблема в том, что sep << args
не понимает, что это потоковая передача.
Ваша другая проблема недостаточна лямбда.
Мы можем исправить это.
template<class F>
struct ostreamer_t {
F f;
friend std::ostream& operator<<(std::ostream& os, ostreamer_t&& self ) {
self.f(os);
return os;
}
template<class T>
friend auto operator<<(ostreamer_t self, T&& t) {
auto f = [g = std::move(self.f), &t](auto&& os)mutable {
std::move(g)(os);
os << t;
};
return ostreamer_t<decltype(f)>{std::move(f)};
}
};
struct do_nothing_t {
template<class...Args>
void operator()(Args&&...)const {}
};
const ostreamer_t<do_nothing_t> ostreamer{{}};
template <typename... Args>
void print(Args... args)
{
std::string sep = " ";
std::string end = "\n";
(std::cout << ... << (ostreamer << sep << args)) << end;
}
живой пример. (Я также использовал литерал для sep
, чтобы я работал с rvalues).
ostreamer
фиксирует ссылки на вещи <<
'd, а затем выгружает их, когда в свою очередь это <<
на ostream
.
Весь этот процесс должен быть прозрачным для компилятора, поэтому достойный оптимизатор должен испарять все, что есть.
Ответ 4
Как утверждают другие, вы пытаетесь использовать неправильный формат выражения.
Вы можете использовать лямбда-помощник для своей цели очень простым способом:
template <typename... Args>
void print(Args&&... args)
{
std::string sep = " ";
std::string end = "\n";
auto streamSep = [&sep](const auto& arg) -> decltype(arg) {
std::cout << sep;
return arg;
};
(std::cout << ... << streamSep(args)) << end;
}
Это будет следовать за поведением, ожидаемым в написанном вами коде. Однако, если вы хотите избежать sep перед первым аргументом, вы можете использовать следующее:
template <typename Arg, typename... Args>
void print(Arg&& arg, Args&&... args)
{
std::string sep = " ";
std::string end = "\n";
auto streamSep = [&sep](const auto& arg) -> decltype(arg) {
std::cout << sep;
return arg;
};
std::cout << arg;
(std::cout << ... << streamSep(args)) << end;
}
Ответ 5
Вы можете попробовать что-то вроде этого
template <typename... Args>
void print(Args... args)
{
bool first = true;
auto lambda = [&](auto param)
{
if( !first) std::cout << ',';
first= false;
return param;
};
((std::cout << lambda(args)), ...);
}
Лямбда гарантирует, что разделитель вставлен только между двумя элементами.
С другой стороны, если вы не хотите использовать lambdas, вы можете перегрузить шаблон:
template<typename T>
void print(T item)
{
std::cout << item;
}
template<typename T, typename... Args>
void print(T item, Args... args)
{
print(item);
std::cout << ',';
print(args...);
}
Ответ 6
Если вы не хотите, чтобы ведущий/конечный sep
:
template <typename First, typename... Rest>
void print(First first, Rest... rest)
{
std::string sep = " ";
std::string end = "\n";
std::cout << first;
((std::cout << sep << rest), ...);
std::cout << end;
}
Вам нужно сделать std::cout << end;
отдельную инструкцию для обработки случая с одним параметром.
Ответ 7
Другой подход заключается в следующем:
#include <iostream>
template<class U, class... T>
void printSpaced(const U& u, const T&... args)
{
using std::cout;
using std::endl;
((cout << u) << ... << (cout << ' ', args)) << endl;
}
Таким образом, вы не получите ведущее/конечное место
Использование:
printSpaced(1, 2, "Hello", 4.5f); //Output 1 2 Hello 4.5 and no trailing space