How to store formatting settings with an IOStream?
Asked Answered
R

1

11

When creating formatted output for a user defined type it is often desirable to define custom formatting flags. For example, it would be nice if a custom string class could optionally add quotes around the string:

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

should produce

example 'example' "example"

It is easy enough to create manipulators for changing the formatting flags themselves:

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

... but how can the manipulators store which quotes should be used with the stream and later have the output operator retrieve the value?

Ranie answered 26/12, 2013 at 22:30 Comment(2)
When I first read the question I was confused because I knew you already knew the answer. LOL :)Penalize
@0x499602D2: the FAQ encourages asking questions and answering them directly (it is even supported by the interface) and I thought this would be question whose answer would be useful to others.Xenophobe
R
14

The streams classes were designed to be extensible, including the ability to store additional information: the stream objects (actually the common base class std::ios_base) provide a couple of functions managing data associated with a stream:

  1. iword() which takes an int as key and yields an int& which starts out as 0.
  2. pword() which takes an int as key and yields a void*& which starts out as 0.
  3. xalloc() a static function which yields a different int on each call to "allocate" a unique key (they keys can't be released).
  4. register_callback() to register a function which is called when a stream is destroyed, copyfmt() is called, or a new std::locale is imbue()d.

For storing simple formatting information as in the String example it is sufficient to allocate an int and store a suitable value in an 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();
}

The implementation uses the stringFormatIndex() function to make sure that exactly one index is allocate as rc is initialized the first time the function is called. Since iword() returns 0 when there is no value, yet, set for a stream, this value is used for the default formatting (in this case to use no quotes). If a quote should be used the char value of the quote is simply stored in the iword().

Using iword() is rather straight forward because there is isn't any resource management necessary. For the sake of example, let's say the String should be printed with a string prefix, too: the length of the prefix shouldn't be restricted, i.e., it won't fit into an int. Setting a prefix is already a bit more involved as a corresponding manipulator needs to be a class type:

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

To create a manipulator with argument an object is created which captures the std::string which is to be used as prefix and an "output operator" is implemented to actually set up the prefix in a pword(). Since there can only be a void* stored, it is necessary to allocate memory and maintain potentially existing memory: if there is already something stored it must be a std::string and it is changed to the new prefix. Otherwise, a callback is registered which is used to maintain the content of the pword() and once the callback is registered a new std::string is allocated and stored in the pword().

The tricky business is the callback: it is called under three conditions:

  1. When the stream s is destroyed or s.copyfmt(other) is called, each registered callback is called with s as the std::ios_base& argument and with the event std::ios_base::erase_event. The objective with this flag is to release any resources. To avoid accidental double release of the data, the pword() is set to 0 after the std::string is deleted.
  2. When s.copyfmt(other) is called, the callbacks are called with the event std::ios_base::copyfmt_event after all callbacks and contents was copied. The pword() will just contain a shallow copy of the original, however, i.e., the callback needs to make a deep copy of the pword(). Since the callback was called with an std::ios_base::erase_event before there is no need to clean anything up (it would be overwritten at this point anyway).
  3. After s.imbue() is called the callbacks are called with std::ios_base::imbue_event. The primary use of this call is to update std::locale specific values which may be cached for the stream. For the prefix maintenance these calls will be ignored.

The above code should be an outline describing how data can be associated with a stream. The approach allows storing arbitrary data and multiple independent data items. It is worth noting that xalloc() merely returns a sequence of unique integers. If there is a user of iword() or pword() which doesn't use xalloc() there is a chance that indices collide. Thus, it is important to use xalloc() to make different code play nicely together.

Here is a live example.

Ranie answered 26/12, 2013 at 22:30 Comment(2)
@0x499602D2: thanks! Trying to compile the code turned out to include a couple of additional errors which I are fixed now.Xenophobe
+1 A SSCCE (preferably as a live example) to see these dark but interesting corners of the library in action, would be much appreciated!Mettlesome

© 2022 - 2024 — McMap. All rights reserved.