如何轻松地使 ofstream 输出有缩进?

17

是否有一种简单的方法可以对输出到 ofstream 对象进行缩进?我有一个以 null 终止并包含换行符的 C++ 字符数组。 我想将其输出到流中,但每行都要缩进两个空格。 是否有一种像使用流特殊指令更改整数输出基数那样使用流操作符来完成此操作的简单方法,或者我必须手动处理该数组,并在检测到每个换行符时手动插入额外的空格?

似乎 string::right() 操作符很接近:

http://www.cplusplus.com/reference/iostream/manipulators/right/

谢谢。

-William


也许现在是时候有人为此编写一个库了 :) - xtofl
1
已经有了,它被称作 facets。它用于格式化流输出。因此流的用户可以像平常一样输出他们的数据。facets 可以独立地执行任何格式(因此仅通过更改流使用的 facet 即可更改输出格式,而无需更改生成输出的代码)。 - Martin York
7个回答

21

这是使用facet的完美情况。

可以将codecvt facet的自定义版本注入到流中。

因此,您的使用方式如下:

int main()
{
    /* Imbue std::cout before it is used */
    std::ios::sync_with_stdio(false);
    std::cout.imbue(std::locale(std::locale::classic(), new IndentFacet()));

    std::cout << "Line 1\nLine 2\nLine 3\n";

    /* You must imbue a file stream before it is opened. */
    std::ofstream       data;
    data.imbue(indentLocale);
    data.open("PLOP");

    data << "Loki\nUses Locale\nTo do something silly\n";
}

该术语的定义比较复杂,但是核心意思是使用该术语的人不需要了解任何格式化相关知识。无论流是如何被使用,格式化都会独立应用。
#include <locale>
#include <algorithm>
#include <iostream>
#include <fstream>

class IndentFacet: public std::codecvt<char,char,std::mbstate_t>
{
  public:
   explicit IndentFacet(size_t ref = 0): std::codecvt<char,char,std::mbstate_t>(ref)    {}

    typedef std::codecvt_base::result               result;
    typedef std::codecvt<char,char,std::mbstate_t>  parent;
    typedef parent::intern_type                     intern_type;
    typedef parent::extern_type                     extern_type;
    typedef parent::state_type                      state_type;

    int&    state(state_type& s) const          {return *reinterpret_cast<int*>(&s);}
  protected:
    virtual result do_out(state_type& tabNeeded,
                         const intern_type* rStart, const intern_type*  rEnd, const intern_type*&   rNewStart,
                         extern_type*       wStart, extern_type*        wEnd, extern_type*&         wNewStart) const
    {
        result  res = std::codecvt_base::noconv;

        for(;(rStart < rEnd) && (wStart < wEnd);++rStart,++wStart)
        {
            // 0 indicates that the last character seen was a newline.
            // thus we will print a tab before it. Ignore it the next
            // character is also a newline
            if ((state(tabNeeded) == 0) && (*rStart != '\n'))
            {
                res                 = std::codecvt_base::ok;
                state(tabNeeded)    = 1;
                *wStart             = '\t';
                ++wStart;
                if (wStart == wEnd)
                {
                    res     = std::codecvt_base::partial;
                    break;
                }
            }
            // Copy the next character.
            *wStart         = *rStart;

            // If the character copied was a '\n' mark that state
            if (*rStart == '\n')
            {
                state(tabNeeded)    = 0;
            }
        }

        if (rStart != rEnd)
        {
            res = std::codecvt_base::partial;
        }
        rNewStart   = rStart;
        wNewStart   = wStart;

        return res;
    }

    // Override so the do_out() virtual function is called.
    virtual bool do_always_noconv() const throw()
    {
        return false;   // Sometime we add extra tabs
    }

};

请参考以下内容:

请查看Tom的下面的注释


这里的预期结果是什么?似乎与此处宣传的不符:http://liveworkspace.org/code/T4tCi$0 - sehe
@sehe: std::cout很有趣。如果流已被使用,则imbue()将无法在任何流上工作。一些实现在main()之前使用std::cout,因此在上面的代码中imbue可能会失败()。但它始终可以在文件上工作。因此,请检查文件PLOP的内容。 - Martin York
我认为我也看到它在使用ostringstream时无法正常工作。稍后我会尝试检查一下。 - sehe
使用clang++编译时,我遇到了以下警告:“警告:'IndentFacet'没有超出行虚拟方法定义;它的虚函数表将在每个翻译单元中被生成[-Wweak-vtables]”。这可以避免吗? - Mathieu Dutour Sikiric
@MathieuDutourSikiric:是的。与其将所有函数放在头文件中,不如将上述内容分成两个文件:一个包含声明的头文件和一个包含定义的源文件。我只是为了方便在stackoverflow上显示而将它们放在一起。但是,我不会费心去做这件事,我会关闭那个警告(这只是意味着如果您更改此类,则需要强制重新构建包括此头文件的所有翻译单元)。如果您不打算更改该类,则不会有问题。 - Martin York

3

好的,虽然这不是我想要的答案,但如果没有其他答案,这里有一种手动完成的方法:

void
indentedOutput(ostream &outStream, const char *message, bool &newline)
{
  while (char cur = *message) {
    if (newline) {
      outStream << "  ";
      newline = false;
    }
    outStream << cur;
    if (cur == '\n') {
      newline = true;
    }
    ++message;
  }
}

2

我已经成功地使用了Martin的codecvt facet建议,但是在OSX上使用std::cout时遇到了问题,因为默认情况下该流使用基于basic_streambuf的streambuf,该streambuf会忽略imbued facet。以下代码将std::cout和其它相关流切换为基于basic_filebuf的streambuf,这将使用imbued facet。

std::ios::sync_with_stdio(false);

伴随副作用的是iostream标准流对象可能会独立于标准C流而操作。

另一个需要注意的是,由于此facet没有静态std::locale::id,这意味着在locale上调用std::has_facet<IndentFacet>总是返回true。添加std::local::id意味着该facet未被使用,因为basic_filebuf寻找基类模板。


谢谢你。我已经寻找这个解决方案很久了。 - Martin York

2
一种添加此功能的方法是编写过滤 streambuf(即将 IO 操作转发到另一个 streambuf 但操作传输的数据)来在其过滤操作中添加缩进。我在这里提供了编写 streambuf 的示例,而 boost 提供了一个来帮助实现。
在您的情况下,overflow() 成员函数只需测试 '\n' 然后在需要时立即添加缩进(与您在indentedOuput函数中执行的操作完全相同,除了 newline 将成为 streambuf 的成员)。您可以可能设置增加或减少缩进大小的选项(可能通过操作符进行访问,该操作符必须进行 dynamic_cast 来确保与流关联的 streambuf 是正确类型;有一种机制可以将用户数据添加到流中——basic_ios::xalloc、iword 和 pword——但这里我们要对 streambuf 进行操作)。

1

没有简单的方法,但有很多关于实现此目标的复杂方式已经被撰写了。阅读这篇文章,其中对该主题进行了良好的解释。这里是另一篇文章,不幸的是,它是用德语写的。但是它的源代码应该能帮到你。

举个例子,你可以编写一个函数来记录递归结构。对于每个递归层级,缩进都会增加:

std::ostream& operator<<(std::ostream& stream, Parameter* rp) 
{
    stream << "Parameter: " << std::endl;

    // Get current indent
    int w = format::get_indent(stream);

    stream << "Name: "  << rp->getName();
    // ... log other attributes as well

    if ( rp->hasParameters() )
    {
        stream << "subparameter (" << rp->getNumParameters() << "):\n";

        // Change indent for sub-levels in the hierarchy
        stream << format::indent(w+4);

        // write sub parameters        
        stream << rp->getParameters();
    }

    // Now reset indent
    stream << format::indent(w);

    return stream; 

}

1
我已将Loki Astarti的解决方案推广为适用于任意缩进级别。该解决方案具有良好易用的界面,但实际实现有些可疑。可以在github上找到它:https://github.com/spacemoose/ostream_indenter
GitHub存储库中有一个更详细的演示,但考虑到:
#include "indent_facet.hpp"

/// This probably has to be called once for every program:
// https://dev59.com/JV8d5IYBdhLWcg3w1VE1
std::ios_base::sync_with_stdio(false);

// This is the demo code:
std::cout << "I want to push indentation levels:\n" << indent_manip::push
          << "To arbitrary depths\n" << indent_manip::push
          << "and pop them\n" << indent_manip::pop
          << "back down\n" << indent_manip::pop
          << "like this.\n" << indent_manip::pop;

}

它生成以下输出:

I want to push indentation levels:
    To arbitrary depths
        and pop them
    back down
like this.

我希望能获得关于代码实用性的任何反馈意见。

虽然使用codecvt进行自动缩进是一个非常好的想法,但是你的源代码存在一些问题:1)在循环中打印制表符时,你没有检查_to缓冲区溢出,这个问题可以很容易地解决;(续在下一条评论中) - segfault
1
修复第一个问题后,libstdc++实现的codecvt还存在另一个问题,即如果上一次操作的结果是std::codecvt_base::partial,则只调用一次do_out,因此如果行中只有几个字符(例如左大括号),而缩进级别很高,则会丢失一些字符。我不知道如何正确解决这个问题,我所知道的唯一丑陋的解决方案是���盖do_max_length()以返回一个大值,强制libstdc++分配足够大的输出缓冲区。 - segfault
@segfault 所有这些疯狂的方案都需要了解这么多细节、角落和明显的错误,真是疯狂。希望使用rangesV3或C++20,你只需执行 cout << (mydata | indent_view) << "\n"; 就可以完成你的工作。 - v.oddou

0

简单的空格处理程序

struct Whitespace
{
    Whitespace(int n)
        : n(n)
    {
    }
    int n;
};

std::ostream& operator<<(std::ostream& stream, const Whitespace &ws)
{
    for(int i = 0; i < ws.n; i++)
    {
        stream << " ";
    }
    return stream;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接