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

How to store formatting settings with an IOStream?

本文关键字:设置 格式 存储 何使用 IOStream      更新时间:2023-10-16

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

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();
}

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

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

iword(),它将
  1. int作为键并产生一个以0开头的int&
  2. pword(),它将
  3. int作为键并产生一个以0开头的void*&
  4. xalloc()一个static函数,它在每次调用时产生不同的int以"分配"唯一密钥(无法释放密钥)。
  5. register_callback()注册在流被销毁、调用copyfmt()imbue()std::locale时调用的函数。

为了存储简单的格式信息,如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()设置为删除std::string0
  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特定值。对于前缀维护,这些调用将被忽略。

上面的代码应该是描述数据如何与流关联的大纲。该方法允许存储任意数据和多个独立的数据项。值得注意的是,xalloc()仅返回一个唯一整数序列。如果iword()pword()的用户不使用xalloc()则索引可能会发生冲突。因此,使用xalloc()使不同的代码很好地协同工作非常重要。

这是一个活生生的例子。