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

10

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

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
当我第一次看到这个问题时,我感到困惑,因为我知道你已经知道答案。哈哈 :) - David G
2
@0x499602D2:常见问题解答鼓励直接提问和回答(甚至在界面上也得到了支持),我认为这个问题的答案会对其他人有用。 - Dietmar Kühl
1个回答

13
流类被设计为可扩展的,包括存储附加信息的能力:流对象(实际上是共同的基类std::ios_base)提供了一些管理与流相关数据的函数:
1. `iword()`接受一个整数作为键并产生一个`int&`,初始值为`0`。 2. `pword()`接受一个整数作为键并产生一个`void*&`,初始值为`0`。 3. `xalloc()` 是一个静态函数,每次调用都会产生一个不同的`int`以“分配”唯一的键(这些键无法释放)。 4. `register_callback()` 用于注册一个函数,当流被销毁时调用该函数,在调用`copyfmt()`或新的`std::locale`进行`imbue()`时也会调用。
对于像`String`示例中存储简单格式信息的情况,只需分配一个`int`并在其中存储适当的值即可。
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。使用此标志的目的是释放任何资源。为避免数据的意外重复释放,在删除 std::string 后将 pword() 设置为 0。 2. 当调用 s.copyfmt(other) 时,使用事件 std::ios_base::copyfmt_event 调用回调函数,在所有回调函数和内容被复制之后。 pword() 将只包含原始数据的浅拷贝,因此回调函数需要进行深度复制。由于在之前使用了 std::ios_base::erase_event 调用了回调函数,因此不需要清理任何内容(此时它将被覆盖)。 3. 在调用 s.imbue() 后,使用 std::ios_base::imbue_event 调用回调函数。这个调用的主要用途是更新可能为流缓存的 std::locale 特定值。对于前缀维护,将忽略这些调用。
上述代码应该是描述如何将数据与流相关联的概要。此方法允许存储任意数据和多个独立数据项。值得注意的是,xalloc() 仅返回唯一整数序列。如果使用 iword()pword() 的用户没有使用 xalloc(),则索引可能冲突。因此,重要的是使用 xalloc() 使不同的代码可以很好地协作。 这里 有一个实时示例。

@0x499602D2:谢谢!尝试编译代码时发现还包括一些额外的错误,现在已经修复了。 - Dietmar Kühl
如果能提供一个最好是实时演示的最小自包含可运行示例(SSCCE),来展示这个库中一些复杂却有趣的部分,那就太感激不尽了! - TemplateRex

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