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

问题描述 投票:0回答:7

我不明白为什么这不起作用。了解模板和可变表达式折叠的人可以解释正在发生的事情并给出可行的解决方案吗?

#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 <<
,但打印时每个参数之间没有空格,它会起作用。

c++ c++17 variadic-templates fold fold-expression
7个回答
36
投票

二进制折叠表达式的语法必须是以下之一:

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

15
投票

这可以工作,但它会打印一个尾随空格:

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

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

现场魔杖盒示例


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

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

11
投票

你真正想做的是:

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

活生生的例子。 (我还使用了

sep
的文字来确保我使用右值)。

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

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


3
投票

正如其他人所回答的,您正在尝试使用错误的 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;
}

2
投票

另一种方法是下一个:

#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

1
投票

你可以尝试这样的事情

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

1
投票

如果您不想要前导/尾随

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;
一条单独的指令来处理具有一个参数的情况。

© www.soinside.com 2019 - 2024. All rights reserved.