如何使用IOStream存储格式设置?

时间:2013-12-26 22:30:08

标签: c++ iostream istream ostream

为用户定义的类型创建格式化输出时,通常需要定义自定义格式标记。例如,如果自定义字符串类可以选择在字符串周围添加引号,那将是很好的:

String str("example");
std::cout << str << ' ' << squotes << str << << ' ' << dquotes << str << '\n';

应该产生

example 'example' "example"

创建操纵器以更改格式标志本身很容易:

std::ostream& squotes(std::ostream& out) {
    // what magic goes here?
    return out;
}
std::ostream& dquotes(std::ostream& out) {
    // similar magic as above
    return out;
}
std::ostream& operator<< (std::ostream& out, String const& str) {
    char quote = ????;
    return quote? out << quote << str.c_str() << quote: str.c_str();
}

...但是操纵器如何存储哪些引号应该与流一起使用,然后让输出操作符检索值?

1 个答案:

答案 0 :(得分:12)

流类设计为可扩展的,包括存储附加信息的能力:流对象(实际上是公共基类std::ios_base)提供了一些管理与流相关的数据的函数:

  1. iword()int为关键字,产生int&,其开头为0
  2. pword()int为关键字,产生void*&,其开头为0
  3. xalloc()一个static函数,在每次调用时产生不同的int以“分配”一个唯一的密钥(它们的密钥无法释放)。
  4. register_callback()注册一个在销毁流时调用的函数,copyfmt()被调用,或者新的std::localeimbue() d。
  5. 为了存储简单的格式信息,如String示例所示,分配int并在iword()中存储合适的值就足够了:

    int stringFormatIndex() {
        static int rc = std::ios_base::xalloc();
        return rc;
    }
    std::ostream& squote(std::ostream& out) {
        out.iword(stringFormatIndex()) = '\'';
        return out;
    }
    std::ostream& dquote(std::ostream& out) {
        out.iword(stringFormatIndex()) = '"';
        return out;
    }
    std::ostream& operator<< (std::ostream& out, String const& str) {
        char quote(out.iword(stringFormatIndex()));
        return quote? out << quote << str.c_str() << quote: out << str.c_str();
    }
    

    该实现使用stringFormatIndex()函数来确保在第一次调用函数时初始化rc时分配一个索引。由于iword()在没有值时返回0,但是为流设置,此值用于默认格式(在这种情况下不使用引号)。如果应使用引用,则引用的char值只会存储在iword()中。

    使用iword()非常简单,因为不需要任何资源管理。例如,假设String也应该用字符串前缀打印:前缀的长度不应该受到限制,即它不适合int。由于相应的操纵器需要是类类型,因此设置前缀已经更加复杂了:

    class prefix {
        std::string value;
    public:
        prefix(std::string value): value(value) {}
        std::string const& str() const { return this->value; }
        static void callback(std::ios_base::event ev, std::ios_base& s, int idx) {
            switch (ev) {
            case std::ios_base::erase_event: // clean up
                delete static_cast<std::string*>(s.pword(idx));
                s.pword(idx) = 0;
                break;
            case std::ios_base::copyfmt_event: // turn shallow copy into a deep copy!
                s.pword(idx) = new std::string(*static_cast<std::string*>(s.pword(idx)));
                break;
            default: // there is nothing to do on imbue_event
                break;
            }
        }
    };
    std::ostream& operator<< (std::ostream& out, prefix const& p) {
        void*& pword(out.pword(stringFormatIndex()));
        if (pword) {
            *static_cast<std::string*>(pword) = p.str();
        }
        else {
            out.register_callback(&prefix::callback, stringFormatIndex());
            pword = new std::string(p.str());
        }
        return out;
    }
    

    要创建带参数的操纵器,会创建一个对象,该对象捕获将用作前缀的std::string,并实现“输出操作符”以在pword()中实际设置前缀。由于只能存储void*,因此必须分配内存并维护可能存在的内存:如果已经存储了某些内容,则必须是std::string并将其更改为新前缀。否则,将注册一个回调,用于维护pword()的内容,一旦注册了回调,就会分配新的std::string并将其存储在pword()中。

    棘手的业务是回调:它在三个条件下被调用:

    1. 当流s被销毁或s.copyfmt(other)被调用时,将使用s作为std::ios_base&参数并使用事件std::ios_base::erase_event调用每个已注册的回调。这个标志的目标是释放任何资源。为避免意外双重释放数据,pword()0被删除后设置为std::string
    2. 调用s.copyfmt(other)时,在复制所有回调和内容后,使用事件std::ios_base::copyfmt_event 调用回调。 pword()只包含原始的浅表副本,但是,回调需要制作pword()的深层副本。由于在没有必要清理任何内容之前使用std::ios_base::erase_event调用了回调(无论如何它都会被覆盖)。
    3. 调用s.imbue()后,将使用std::ios_base::imbue_event调用回调。此调用的主要用途是更新可能为流缓存的std::locale个特定值。对于前缀维护,将忽略这些调用。
    4. 上述代码应该是描述数据如何与流关联的大纲。该方法允许存储任意数据和多个独立数据项。值得注意的是,xalloc()仅返回一系列唯一整数。如果有iword()pword()的用户不使用xalloc(),则索引可能会发生冲突。因此,使用xalloc()使不同的代码很好地协同工作非常重要。

      Here是一个实例。