C++
6 ноября 2011

std::stringstream и форматирование строк

Ввод и вывод информации — критически важная задача, без выполнения которой любая программа становится бесполезной. В C++ для решения данной задачи традиционно применяются потоки ввода-вывода, которые реализованы в стандартной библиотеке IOStream.

Плюсами такого подхода являются:
— универсальность: не важно, с чем связан поток, — ввод-вывод с консоли, файла, сокета, процесса происходит одинаково;
— прозрачность: программисту не нужно явно указывать тип вводимого или выводимого объекта;
— расширяемость: программист может добавить поддержку ввода-вывода в поток для любого своего объекта, просто перегрузив соответствующие операторы.

В библиотеке IOStream есть также класс stringstream, который позволяет связать поток ввода-вывода со строкой в памяти. Всё, что выводится в такой поток, добавляется в конец строки; всё, что считыватся из потока — извлекается из начала строки.

Он позволяет делать весьма забавные вещи, например, осуществлять преобразование типов:



#include <sstream>
#include <iostream>

int main(int argc, char* argv[])
{
    std::stringstream ss;
    ss << "22";
    int k = 0;
    ss >> k;
    std::cout << k << std::endl;
    return 0;
}


Кроме того, этот класс можно использовать для форматирования сложных строк, например:

void func(int id, const std::string& data1, const std::string& data2)
{
    std::stringstream ss;
    ss << "Operation with id = " << id << " failed, because data1 (" << data1 << ") is incompatible with data2 (" << data2 << ")";
    std::cerr << ss.str();
}


Понятно, что в данном случае использование stringstream излишне, так как сообщение можно было выводить напрямую в cerr. Но что если вы хотите вывести сообщение не в стандартный поток, а использовать, скажем, функцию syslog() для вывода сообщения в системный журнал? Или, скажем, сгенерировать исключение, содержащее данную строку как пояснение:

void func(int id, const std::string& data1, const std::string& data2)
{
    std::stringstream ss;
    ss << "Operation with id = " << id << " failed, because data1 (" << data1 << ") is incompatible with data2 (" << data2 << ")";
    throw std::runtime_error(ss.str());
}


Получается, вам необходим промежуточный объект stringstream, в котором вы сначала формируете строку, а затем получаете её с помощью метода str(). Согласитесь, громоздко?

C++ порой винят за его чрезмерную сложность и избыточность, которые, однако, в ряде случаев позволяют создавать весьма изящные конструкции. Вот и сейчас, шаблоны и возможность перегрузки операторов помогли мне создать обёртку над stringstream, которая позволяет форматировать строку в любом месте кода без дополнительных переменных как если бы я просто выводил данные в поток:

void func(int id, const std::string& data1, const std::string& data2)
{
    throw std::runtime_error(MakeString() << "Operation with id = " << id << " failed, because data1 (" << data1 << ") is incompatible with data2 (" << data2 << ")");
}


А вот и сам MakeString:

class MakeString {
public:
    template<class T>
    MakeString& operator<< (const T& arg) {
        m_stream << arg;
        return *this;
    }
    operator std::string() const {
        return m_stream.str();
    }
protected:
    std::stringstream m_stream;
};


Работает это очень просто. С одной стороны, в классе MakeString перегружен оператор вывода (<<), который принимает в качестве аргумента константную ссылку на объект любого типа, тут же выводит этот объект в свой внутренний stringstream и возвращает ссылку на себя. С другой стороны, перегружен оператор преобразования к строке, который возвращает строку, сформированную stringstream'ом.

Быть может, кому-то эта обёртка окажется полезной. Буду рад услышать комментарии, предложения и пожелания.
+24
88,9k 114
Комментарии 32