C++ fstream - 创建自定义格式化标志

6

我需要为输出文件的格式创建新的标记。我有一个类。

class foo{
    bar* members;
    ofstream& operator<<(ofstream&);
    ifstream& operator>>(ifstream&);
};

我想要像这样使用它:

fstream os('filename.xml');
foo f;
os << xml << f;
os.close();

这将保存一个XML文件。

fstream os('filename.json');
foo f;
os << json << f;
os.close();

这是一个 JSON 文件。

我该如何做到这一点?

3个回答

9

你可以轻松地创建自己的操作器,可以劫持现有标志或使用std::ios_base::xalloc来获取新的流特定内存,例如(在Foo的实现文件中:

static int const manipFlagId = std::ios_base::xalloc();

enum
{
    fmt_xml,        //  Becomes the default.
    fmt_json
};

std::ostream&
xml( std::ostream& stream )
{
    stream.iword( manipFlagId ) = fmt_xml;
    return stream;
}

std::ostream&
json( std::ostream& stream )
{
    stream.iword( manipFlagId ) = fmt_json;
    return stream;
}

std::ostream&
operator<<( std::ostream& dest, Foo const& obj )
{
    switch ( dest.iword( manipFlagId ) ) {
    case fmt_xml:
        // ...
        break;
    case fmt_json:
        //  ...
        break;
    default:
        assert(0);  //  Or log error, or abort, or...
    }
    return dest;
}

在头文件中声明xmljson,任务就完成了。

尽管如此,我认为这有点滥用操作器。像xml这样的格式超越了简单的本地格式化,最好由一个独立的类来处理,该类拥有ostream,并写入整个流,而不仅仅是单个对象。


1

这个问题是iostream库中最大的缺陷。

James Kanze的解决方案是部分解决方案,适用于您自己的类,但通常对象都有一种不同的流方式。

我的常规方法是创建自己的包装器类,并使用一个函数将其传递到您的流中,对于xml,它将包含xml_node()xml_attribute()的重载。

os << xml_attribute( "Id", id );

将属性Id设置为xml格式变量中的任何内容。

我还编写了节点范围,因此它们将在构建时将节点打开文本写入流,并在销毁时自动编写关闭逻辑。

我的方法优于James Kanze的解决方案的优点是可扩展性。我认为James Kanze的结论表明他不支持他的解决方案,可能会使用类似于我的解决方案。

使用上述解决方案,为了添加更多格式,您必须在各个operator<<函数中进行编辑,而json格式化代码将是完全不同的一组函数,如果您添加了另一种格式,则会添加其代码,而无需编辑任何现有代码。

顺便说一下,对于输入,对于XML,您将使用现有的DOM或SAX解析器,并且不会以这种方式直接使用iostream。


我认为直接使用流来输出XML并不是一个合适的解决方案。流是流的抽象,而不是分层数据结构。对于输出XML,我会使用像Xerces这样的库;它允许在分层结构中插入内容。至于其余部分:如果您需要内置类型的非常规格式或非常复杂的结构,则包装器类很有用,但操作符在大多数情况下也能很好地工作。 - James Kanze
更准确地说(也许这就是你的意思):输出到全局不同格式应该使用不同的机制而不是流:自定义操作符是用于单个自定义类型的,而不是影响所有类型的输出。 (而且你不能为“int”添加格式选项)。 - James Kanze

0
最简单的方法是首先创建一个标签类型和其单个实例:
struct JsonStreamTag {} json;

然后让这样的标签构建一个对象来包装流:

class JsonStream {
public:

    // (1)
    friend JsonStream operator<<(std::ostream& ostream, const JsonStreamTag&) {
        return JsonStream(ostream);
    }

    // (2)
    template<class T>
    friend JsonStream& operator<<(JsonStream& json_stream, const T& value) {
        write_json(json_stream.ostream, value); // (3)
        return json_stream;
    }

protected:

    JsonStream(std::ostream& ostream) : ostream(ostream) {}

private:

    std::ostream& ostream;

};

构造函数是protected的,以确保您只能使用some_ostream << json(1)来构造JsonStream。另一个插入运算符(2)执行实际格式化。然后,您为每个相关类型定义write_json()(3)的重载:
void write_json(std::ostream& stream, int value) {
    stream << value;
}

void write_json(std::ostream& stream, std::string value) {
    stream << '"' << escape_json(value) << '"';
}

// Overloads for double, std::vector, std::map, &c.

或者省略 (2),转而为 operator<<(JsonStream&, T) 添加重载。
然后,按照相同的过程编写相应的 XmlStream ,并使用 XmlStreamTagwrite_xml()。 这假定您的输出可以完全从您正在写入的特定值构造; 如果您需要一些标题或页脚在每个文件中都相同,请使用构造函数和析构函数:
XmlStream(std::ostream& ostream) : ostream(ostream) {
    ostream << "<?xml version=\"1.0\"?><my_document>"
}

~XmlStream() {
    ostream << "</my_document>";
}

1
我认为这不是一个很好的想法。例如,对于像 out << json << "Header: " << obj; 这样的东西是行不通的。(当然,对于 XML 这样的东西,这本来就没有意义。但这也是不使用操作符的一个论据。) - James Kanze
@JamesKanze:这个问题没有明确定义。为了字面上回答这个问题,我假设“JSON流”会将所有内容都视为JSON值。但我认为这样的做法本身就是错误的。 - Jon Purdy
XML(以及JSON,据我所知)不是流;它们是更复杂的结构。不相关的类型不应该将XML或JSON输出到“ostream”中;它应该插入到XML或JSON数据结构的某个实例中,然后在文件级别处理流输出。 - James Kanze
@JamesKanze:我同意。流适用于扁平数据,而JSON和XML都是固有的嵌套结构。您可以使用RAII来启用基于范围的嵌套 - {std :: cout << json :: object << ...;} - 但我认为与仅使用正确的数据结构相比,这没有任何优势。 - Jon Purdy

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