使用 std::ostream 打印可变参数包的最简单方法是啥?

Posted

技术标签:

【中文标题】使用 std::ostream 打印可变参数包的最简单方法是啥?【英文标题】:What is the easiest way to print a variadic parameter pack using std::ostream?使用 std::ostream 打印可变参数包的最简单方法是什么? 【发布时间】:2015-02-07 02:49:22 【问题描述】:

使用std::ostream 打印参数包的最简单方法是什么,以逗号分隔?

例子:

template<typename... Args>
void doPrint(std::ostream& out, Args... args)
   out << args...; // WRONG! What to write here?


// Usage:
int main()
   doPrint(std::cout,34,"bla",15); // Should print: 34,bla,15

注意: 可以假设 &lt;&lt; 运算符的相应重载可用于所有类型的参数包。

【问题讨论】:

在 C++17 中,你会说 (out &lt;&lt; ... &lt;&lt;&lt; args); 注意:这不是重复的——foo &lt;&lt; X &lt;&lt; Y; 通常与foo &lt;&lt; X; foo &lt;&lt; Y; 不同,尤其是当foo 具有诸如打开磁盘上的文件等副作用时。 @MSalters,foo &lt;&lt; X &lt;&lt; Y 也没有指定 XY 的评估顺序(即在 C++17 之前) 【参考方案1】:

没有你想要的递归调用和逗号。

在c++11/c++14通过参数包扩展:

template <typename Arg, typename... Args>
void doPrint(std::ostream& out, Arg&& arg, Args&&... args)

    out << std::forward<Arg>(arg);
    using expander = int[];
    (void)expander0, (void(out << ',' << std::forward<Args>(args)), 0)...;

DEMO


在c++17 中使用折叠表达式:

template <typename Arg, typename... Args>
void doPrint(std::ostream& out, Arg&& arg, Args&&... args)

    out << std::forward<Arg>(arg);
    ((out << ',' << std::forward<Args>(args)), ...);

DEMO 2

【讨论】:

这个是最简单的。只是看起来怪怪的,呵呵。 @GermánDiago en.cppreference.com/w/cpp/language/parameter_pack 给出了一个类似的例子:int dummy[sizeof...(Ts)] = (std::cout &lt;&lt; args, 0)... ; 附带说明,您可以跳过using 声明,直接写(void)(int[])(std::cout &lt;&lt; ',' &lt;&lt; std::forward&lt;Args&gt;(args)), 0)...;。您也不需要大括号中的第一个零或内部 void 演员表。 @AlexanderRevo insertingostream 意味着将参数传递给operator&lt;&lt; 的某个重载。你永远不知道操作员期望什么参数类型 我的 C++ 一定已经过时了。我根本无法解析。【参考方案2】:

在 C++17 中,将有一种更简单的方法(正如 Kerrek SB 在 cmets 中所暗示的那样;这实际上出现在 N4606,第一个后 C++14 草案中),称为 fold expressions

代码是:

(out << ... << args);

而模式expression op...op parameter-pack称为二元左折叠,其定义等价于@987654326 @ expression op arg1) op arg2) op arg3) .... op argN.

我认为对于这样的表达式语句来说,外括号并不是绝对必要的,但如果折叠表达式是另一个运算符的操作数,那么它们要么是必需的,要么是一个非常好的主意:)

【讨论】:

使用分隔符:((out &lt;&lt; ", " &lt;&lt; args), ...);【参考方案3】:

通常的答案是定义两个单独的重载,一个空的作为基本情况:

// base case
void doPrint(std::ostream& out) 

template <typename T, typename... Args>
void doPrint(std::ostream& out, T t, Args... args)

    out << t;                // add comma here, see below
    doPrint(out, args...);

当然,在实际代码中,我不会每次都复制参数,而是使用转发引用,但你明白了。

如果您想在每一项之后添加逗号,即使是在最后一项之后,只需将 out &lt;&lt; t 替换为 out &lt;&lt; t &lt;&lt; ','

如果您只想在内部使用逗号,而不是在最后一个元素之后,则需要一个单独的不打印逗号的单参数重载,并且通用重载在包之前采用两个不同的参数,即:

template <typename T>
void doPrint(std::ostream& out, T t)

    out << t;


template <typename T, typename U, typename... Args>
void doPrint(std::ostream& out, T t, U u, Args... args)

    out << t << ',';
    doPrint(out, u, args...);

【讨论】:

你能详细说明一下转发参考吗?我对 C++11 的东西还不是很坚定,但是复制参数似乎有点过头了。 @gexicide:这个网站上有数千个重复项,只需四处搜索。您正在寻找Args &amp;&amp;... argsstd::forward&lt;Args&gt;(args)...【参考方案4】:

参数包扩展仅适用于普通函数调用,不适用于中缀运算符。因此,您需要将s &lt;&lt; x 语法转换为普通函数调用语法f(s, x)

template<class Head>
void print_args_(std::ostream& s, Head&& head) 
    s << std::forward<Head>(head);


template<class Head, class... Tail>
void print_args_(std::ostream& s, Head&& head, Tail&&... tail) 
    s << std::forward<Head>(head);
    print_args_(s, std::forward<Tail>(tail)...);


template<class... Args>
void print_args(Args&&... args) 
    print_args_(std::cout, std::forward<Args>(args)...);

【讨论】:

【参考方案5】:

我知道这是一个老问题,但它对我的问题帮助很大。我根据这篇帖子的答案创建了一个实用程序类,我想分享我的结果。

考虑到我们使用 C++11 或更高版本的 C++,这个类提供了 print 和 println 函数来在调用标准输出流之前组合字符串,避免并发问题。这些是可变参数函数,它们使用模板来打印不同的数据类型。

您可以在我的 github 上查看它在生产者-消费者问题中的用途:https://github.com/eloiluiz/threadsBar

所以,这是我的代码:

class Console 
private:
    Console() = default;

    inline static void innerPrint(std::ostream &stream) 

    template<typename Head, typename... Tail>
    inline static void innerPrint(std::ostream &stream, Head const head, Tail const ...tail) 
        stream << head;
        innerPrint(stream, tail...);
    

public:
    template<typename Head, typename... Tail>
    inline static void print(Head const head, Tail const ...tail) 
        // Create a stream buffer
        std::stringbuf buffer;
        std::ostream stream(&buffer);
        // Feed input parameters to the stream object
        innerPrint(stream, head, tail...);
        // Print into console and flush
        std::cout << buffer.str();
    

    template<typename Head, typename... Tail>
    inline static void println(Head const head, Tail const ...tail) 
        print(head, tail..., "\n");
    
;

比起重载&lt;&lt; 运算符或使用复杂的流函数,我更喜欢这种替代方法。它是一种递归方法,但并不难理解。

【讨论】:

我会更改= delete 中的构造函数。由于只有静态方法,实例化Console对象是没有意义的【参考方案6】:

同样适用于std::wostream 的通用形式:

template <typename CharT, typename Traits>
std::basic_ostream<CharT, Traits> &
Print(std::basic_ostream<CharT, Traits> &out)

    return out;


template <typename CharT, typename Traits, typename T>
std::basic_ostream<CharT, Traits> &
Print(std::basic_ostream<CharT, Traits> &out, T &&t)

    return (out << std::forward<T>(t));


template <typename CharT, typename Traits, typename T, typename... Args>
std::basic_ostream<CharT, Traits> &
Print(std::basic_ostream<CharT, Traits> &out, T &&t, Args &&...args)

    return Print( Print(out, std::forward<T>(t)), std::forward<Args>(args)... );

在常见情况下,我无法使其与 std::endl 一起使用(在特定情况下可以处理 std::endl,例如当它是第一个或最后一个参数时,但不是在常见情况下,尤其是如果单个呼叫中有多个std::endl)。如果您确实需要 std::endl,您仍然可以使用 '\n' 或使用 std::endl 指定模板参数:

Print(std::cout, "hello world", std::endl<char, std::char_traits<char>>);

std::endl'\n' 之间的 differences

如果流在二进制模式下工作,则'\n' 不会转换为编译代码所针对的平台的行尾格式(但在文本模式下仍会转换)。 '\n' 不会用std::flush 刷新流(但如果程序在终端上运行,它仍然是flushes std::cout

所以对我来说可以使用'\n',甚至可以使用preferred。

仍然可以使用其他一些 IO 操纵器:

Print(std::cout, std::hex, 11, '\n');

我还实现了sprintf 对应物,它与可变参数模板一起使用并返回std::string

template <typename CharT = char, typename Traits = std::char_traits<CharT>, typename... Args>
std::basic_string<CharT, Traits>
SPrint(Args &&...args)

    std::basic_stringstream<CharT, Traits> ss;
    Print(ss, std::forward<Args>(args)...);
    return std::move(ss.str());

这里有一些demos。

【讨论】:

以上是关于使用 std::ostream 打印可变参数包的最简单方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

‘operator<<’ 不匹配(操作数类型是 ‘std::ostream’ aka ‘std::basic_ostream<char>’ 和 ‘const std::type

错误:'operator<<' 不匹配(操作数类型为'std::ostream' aka'std::basic_ostream<char>' 和'std::_List_iter

错误:无法将‘std::basic_ostream<char>’左值绑定到‘std::basic_ostream<char>&&’

boost::interprocess::file_lock 与 std::ostream 一起使用时的错误行为

使用 Boost HOF 实现 STL 漂亮打印

打印性能优化[缓冲模式角度]