C++17 可变参数模板折叠

C++17 Variadic Template Folding

我不明白为什么这不起作用。了解模板和可变表达式折叠的人可以解释发生了什么并提供有效的解决方案吗?

#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);
}

它应该打印出每个参数,中间有一个 space,末尾有一个换行符。如果您删除 sep << 则它会起作用,但打印时每个参数之间没有 space。

这会起作用,但它会打印尾随 space:

template <typename... Args>
void print(Args... args)
{
    std::string sep = " ";
    std::string end = "\n";

    ((std::cout << args << sep), ...) << end;
}

live wandbox example


在这种情况下,将执行 逗号运算符 的折叠,从而导致扩展如下:

// (pseudocode)
(std::cout << args<0> << sep), 
(std::cout << args<1> << sep),
(std::cout << args<2> << sep), 
...,
(std::cout << args<N> << sep), 

二进制 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...);
    }
}

你真正想做的是:

std::string sep = " ";
std::string end = "\n";
(std::cout << ... << (sep << args)) << end;

因为你希望 (sep << args)std::cout 左折叠。这不起作用,因为 sep << args 根本不知道它正在流式传输到 std::cout 或流式传输; << 仅当左侧是流时才流式传输。

总之问题是sep << args没看懂是流

你的另一个问题是 lambda 不够。

我们可以解决这个问题。

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;
}

live example。 (我还使用了 sep 的文字来确保我使用右值)。

ostreamer 捕获对它是 << 的事物的引用,然后在它是 << 时将它们转储到 ostream.

整个过程对编译器来说应该是透明的,因此一个好的优化器应该消除所有涉及的东西。

正如其他人的回答,您正在尝试使用错误的 fold-expression 格式。 您可以通过一种非常简单的方式使用 lambda 帮助程序来达到您的目的:

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;
}

你可以试试这样的

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)), ...);
}

lambda 确保仅在两个项目之间插入分隔符。

另一方面,如果您不想使用 lambda,您可以重载模板:

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...);
}

如果你不想leading/trailing 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;一条单独的指令来处理带有一个参数的大小写。

另一种做法是下一个:

#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;   
}

这样你就不会得到leading/trailingspace
用法:

printSpaced(1, 2, "Hello", 4.5f); //Output 1 2 Hello 4.5 and no trailing space