2017-05-01 55 views
19

我不明白为什么这不起作用。能理解模板和可变表达式折叠的人能解释发生了什么,并给出可行的解决方案吗?C++ 17可变模板折叠

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

回答

26

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

对于'(std :: cout << sep << args,...);'它说“表达式不允许作为折叠表达式的操作数”。然而,它表明我把括号放在它的一部分上,比如'((std :: cout << sep << args),...);'但是这会跳过打印第一个参数。有没有这一行的版本可以工作? – nickeb96

+0

@ nickeb96对,忘了括号。我不确定你对其余的意思,它不会跳过第一个论点。 – Barry

+1

啊,我得到了第二部分的工作。它打印一个领先的'sep'然而。有没有一种简单的方法可以在每个参数之间使用'sep'?还是需要一个更复杂的解决方案,如递归? – nickeb96

14

这将工作,但它会打印尾随空格:

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), 
+0

'args <0>'实际上是做了些什么还是仅仅是伪代码中的一个例子? – nickeb96

+1

@ nickeb96:只是伪代码。 –

10

你真正想要做的是:

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的文字来确保我用rvalues工作)。

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

整个过程对于编译器来说应该是透明的,所以体面的优化器应该会消除所有涉及的事情。

3

正如其他人回答的那样,您尝试使用错误的折叠表达式格式。 你可以使用你的目的拉姆达帮手一个非常简单的方法:

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

这会按照预期你写的代码的行为。但是,如果你想避免的第一个参数前,你可以使用以下命令:

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

我不是lambdas的专家,但假设你可以将lambda变成一个函数并把它放在打印函数的上方?或者这会更复杂和/或产生更差的组装? – nickeb96

+1

@ nickeb96如果我们想从这个想法开始优化这个例子(仅仅因为我有两个_unoptimized_参数_sep_和_end_,而我不知道如何处理它们),那么我们可以很容易地获得相同的程序集(根据到GCC 7.1)使用函数而不是lambda。看[这个例子](https://godbolt.org/g/TGoYLT):这两个版本可以编译定义或不定义USE_LAMBDA,你可以看到结果是一样的。但是,我不确定我是否回答了您的疑问,因为我使用_sep_和_end_作为文字。 – dodomorandi

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表达式,你可以重载模板:

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