Как сохранить настройки форматирования с помощью IOStream?

При создании форматированного вывода для определенного пользователем типа часто желательно определить пользовательские флаги форматирования. Например, было бы неплохо, если бы пользовательский класс строки мог дополнительно добавлять кавычки вокруг строки:

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

... но как манипуляторы могут сохранить, какие кавычки следует использовать с потоком, а затем заставить оператор вывода получить значение?


person Dietmar Kühl    schedule 26.12.2013    source источник
comment
Когда я впервые прочитал вопрос, я был сбит с толку, потому что знал, что вы уже знаете ответ. РЖУ НЕ МОГУ :)   -  person 0x499602D2    schedule 27.12.2013
comment
@ 0x499602D2: FAQ поощряет задавать вопросы и отвечать на них напрямую (это даже поддерживается интерфейсом), и я подумал, что это вопрос, ответ на который будет полезен другим.   -  person Dietmar Kühl    schedule 27.12.2013


Ответы (1)


Классы потоков были разработаны с возможностью расширения, включая возможность хранения дополнительной информации: объекты потока (на самом деле общий базовый класс std::ios_base) предоставляют пару функций, управляющих данными, связанными с потоком:

  1. iword(), который принимает int в качестве ключа и дает int&, который начинается как 0.
  2. pword(), который принимает int в качестве ключа и дает void*&, который начинается как 0.
  3. xalloc() функция static, которая выдает разные int при каждом вызове для «выделения» уникального ключа (эти ключи не могут быть освобождены).
  4. register_callback() для регистрации функции, которая вызывается при уничтожении потока, вызывается copyfmt() или новым std::locale является imbue()d.

Для хранения простой информации о форматировании, как в примере 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, которые могут кэшироваться для потока. При обслуживании префикса эти вызовы будут игнорироваться.

Приведенный выше код должен быть схемой, описывающей, как данные могут быть связаны с потоком. Подход позволяет хранить произвольные данные и несколько независимых элементов данных. Стоит отметить, что xalloc() просто возвращает последовательность уникальных целых чисел. Если есть пользователь iword() или pword(), который не использует xalloc(), есть шанс, что индексы столкнутся. Таким образом, важно использовать xalloc(), чтобы разные коды хорошо сочетались друг с другом.

Вот живой пример.

person Dietmar Kühl    schedule 26.12.2013
comment
@ 0x499602D2: спасибо! Попытка скомпилировать код обнаружила пару дополнительных ошибок, которые сейчас исправляю. - person Dietmar Kühl; 27.12.2013
comment
+1 SSCCE (желательно в качестве живого примера), чтобы увидеть эти темные, но интересные уголки библиотеки в действии, был бы очень признателен! - person TemplateRex; 27.12.2013