6 ноября 2011 в 10:21

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

C++*
Ввод и вывод информации — критически важная задача, без выполнения которой любая программа становится бесполезной. В 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'ом.

Быть может, кому-то эта обёртка окажется полезной. Буду рад услышать комментарии, предложения и пожелания.
Алексей Осипов @lionsimba
карма
26,0
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (32)

  • +7
    Есть большущий минус, если захочется локализовать приложение, то такой вариант никак не подойдет. Для этого и придумано семейство printf, с другой стороны С++ не позволяет указать порядок. А вот boost::format это делает с легкостью, многие почему-то боятся boost, но данная библиотека очень сильно экономит время при разработке ПО.
    • +1
      Классно! Спасибо за наводку на boost::format.

      #include <boost/format.hpp>
      
      class MakeString2 {
      public:
          MakeString2(const char* fmt): m_fmt(fmt) {}
      
          template<class T>
          MakeString2& operator<< (const T& arg) {
              m_fmt % arg;
              return *this;
          }
          operator std::string() const {
              return m_fmt.str();
          }
      protected:
          boost::format m_fmt;
      };
      


      void func(int id, const std::string& data1, const std::string& data2)
      {
          throw std::runtime_error(MakeString2("Operation with id = %1% failed, because data1 (%2%) is incompatible with data2 (%3%)") << id << data1 << data2);
      }
      
      • +1
        Более того, аналогичная описанной в статье обёртка над std::stringstream находится где-то глубоко внутри boost::lexical_cast.
        • +2
          Вертно, только для базовых типов там есть оптимизация, которая делает преобразование без использования stringstream.
  • 0
    Могут возникнуть проблемы при форматированном выводе дробных чисел, указателей и т.д.
    Имхо, printf более конкретен.
    • +1
      А iomanip не спасает, разве?
  • 0
    В С++ не бум-бум, но недавно увидел, что с помощью этой библиотеки удачно делаь строку на подстроку через делитель. Может кто-либо подсказать более изящнее решение на отсутствие таких стандартных функций как split; explode etc.?
    • +1
    • +1
      boost::tokenizer или boost::split из набора строковых алгоритмов, недостающих в C++ (boost string algo): www.boost.org/doc/libs/1_47_0/doc/html/string_algo.html

      Первый мне не понравился, в нем нельзя разделители предикатом задавать (мне нужно было ::std::is_punct), а он умеет только массив символов принимаь.

      Со вторым все ок.

    • 0
      При работе с вектором, например, очень удобно можно использовать ostream_iterator
  • 0
    Года 3 назад, тоже такую же обертку где-то у себя в проектах делал. Проблем не возникло.
  • +1
    Use boost::lexical_cast, Luke!
    Он на порядок(!) быстрее стрингстрима за счет того, что не требует создания тяжелого объекта. Если не учитывать создание объекта, то он все равно быстрее, хотя уже на десятки процентов.

    Сравнение внизу страницы: www.boost.org/doc/libs/1_47_0/libs/conversion/lexical_cast.htm
    • +2
      Изначально lexical_cast так и работал, со временем для стандартных типов добавили частную спецификацию.
  • 0
    Месяцем раньше и я бы вас расцеловал :)
    • +2
      Вот сэкономьте поцелуи, пробегитесь за пару дней по документации boost, хотя бы просто по общему описанию всех библиотек, чтобы знать когда куда копать.

      Не тратьте поцелуи зря)
      • 0
        А есть ли в boost что-нибудь похожее на MakeString или MakeString2?
        • 0
          boost::lexical_cast(«123»);
          boost::lexical_cast(123);
          • 0
            А как использовать boost::lexical_cast для форматирования строки?

            Будьте добры, перепишите мой пример с выбрасыванием исключения с использованием boost::lexical_cast.
            • 0
              void func(int id, const std::string& data1, const std::string& data2)
              {
                  throw std::runtime_error("Operation with id = " + boost::lexical_cast<std::string>(id) + " failed, because data1 (" + data1 + ") is incompatible with data2 (" + data2 + ")");
              }
              
              • 0
                Угу. Только нужно первый строковый операнд ещё принудительно привести к std::string, т.к. для const char* не определен оператор "+".

                Но я впрочем, не об этом. Положим, data1 и data2 имеют сложный тип (пусть это будут экземпляры классов A и B). Тогда имеем:

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


                Это сработает, если для A и B определены операторы вывода в поток (<<).

                А теперь сравните то же самое с использованием предлагаемого класса MakeString:
                void func(int id, const A& data1, const B& data2)
                {
                    throw std::runtime_error(MakeString() << "Operation with id = " << id << " failed, because data1 (" << data1 << ") is incompatible with data2 (" << data2 << ")");
                }
                


                По-моему, получилось значительно компактнее, разве нет?

                Я просто хочу подчеркнуть, что целью данного топика было представить класс MakeString() в контексте его использования для форматирования строк, а не использование std::stringstream для преобразования типов.

                MakeString() позволяет форматировать строку, как если бы она была потоком.

                MakeString — 'inplace' конвертер из std::ostream в std::string. Вот в чем суть. Наверное, стоит добавить эти разъяснения в топик.
                • 0
                  Угу. Только нужно первый строковый операнд ещё принудительно привести к std::string, т.к. для const char* не определен оператор "+".
                  Не нужно, я же проверил на компилябельность перед отправкой комментария.

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

                    А я поленился :)

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


                    Беда в том, что boost::format, как выяснилось, тоже не умеет автоматически преобразовываться к строке:

                    std::cout << boost::format("%2% - %1%") % 1 % 2 << std::endl; // работает
                    
                    ...
                    
                    boost::format fmt("%2% - %1%");
                    fmt  % 1 % 2;
                    std::string str = fmt.str() // работает
                    
                    ...
                    
                    boost::format fmt("%2% - %1%");
                    std::string str = fmt  % 1 % 2; // не работает
                    
                    ...
                    
                    std::string str = boost::format("%2% - %1%")  % 1 % 2; // не работает
                    
                    ...
                    
                    throw std::runtime_error(boost::format("%2% - %1%")  % 1 % 2); // не работает
                    
                    ...
                    
                    std::string str = MakeString2("%2% - %1%") << 1 << 2; // работает (MakeString2 определен в моем комментарии выше)
                    
                    • +2
                      using boost::str;

                      throw std::runtime_error(str(boost::format("%2% — %1%") % 1 % 2));

                      И велосипед не нужен=)
                      • 0
                        Во! Спасибо. Этого я и добивался от f0b0s.
  • 0
    Да, классы хорошие. С их помощью очень удобно делать логгер, который понимает любые типы, которые умеют сериализоваться в std::ostream

    Например так:
    #include <string>
    #include <iostream>
    #include <sstream>
    using namespace std;
    
    class LogMessage
    {
    public:
    	~LogMessage()
    	{
    		cout << msg << endl;
    	}
    	
    	static LogMessage log()
    	{
    		return LogMessage();
    	}
    
    	template<class T>
    	LogMessage& operator<<(const T& obj)
    	{
    		std::ostringstream ostr;
    		ostr << obj;
    		if(!msg.empty())
    			msg+=" ";
    		msg+=ostr.str();
    		return *this;
    	}
    	
    private:
    	LogMessage(){};
    	std::string msg;
    };
    
    LogMessage log()
    {
    	return LogMessage::log();
    }
    
    
    struct Custom
    {
    	Custom():x(33), name("some_obj"){}
    	
    	int x;
    	std::string name;
    };
    
    std::ostream& operator<< (std::ostream& ostr, const Custom& obj)
    {
    	return ostr << "x="<<obj.x<<", name: " << obj.name;
    }
    
    int main()
    {
    	log() << "hey" << 33;
    	
    	Custom customObject;
    	customObject.x = 37;
    	
    	log() << 3.4 << " and " << customObject;
    }
    
    • +2
      Боже, зачем вы каждый раз создаете ::std::ostringstream, тем более, если это метод, а не свободная функция?

      Ну сделайте вы его членом, да очищайте через ostream.str(::std::string()).

      Тому, кто скажет, что это преждевременная оптимизация, я отвечу, что не сделать этого — преждевременная пессимизация, так как перенести 1 строку легко, а искать почему с логами тормозит, а без них нет профилировщиком — бесполезная трата времени получится.
      • –1
        Есть принцип — создавать объекты в наименьшей области видимости. Это говорит компилятору о том, что где нужно, и дальше он сам уже может оптимизировать.
        Насчет данного примера — логи тормозят в момент вывода на консоль или в момент записи в лог файл. Поэтому иногда приходится делать их асинхронными, но вот создание временных объекто не тормозило ни разу.

        PS. Если вам ну так сильно хочется оптимизровать код — то можно сразу же заменить код фукнции на boost::lexical_cast — он в разы быстрее, и точней отражает сущность нужного нам преобразования.
        • НЛО прилетело и опубликовало эту надпись здесь
      • –1
        Если вы так боитесь преждевременной пессимизации в связи с созданием временных переменных, то предлагаю при старте программы выделять массив данных, и потом все переменные использовать как элементы этого массива.
        • 0
          Вы можете шуть сколько угодно, но я натыкался на горло, когда создание стрингстрима тормозило. Конкретно он — тяжелый объект.

          Касательно компилятора — оптимизаторы значительно умнее, чем вы, вероятно, думаете, и по CFG спокойно можно понять, где что используется и без областей видимостей, не говорите глупостей.
  • +1
    в качестве альтернативы хитрожопому классу с неявным преобразованием к std::string могу предложить хитрожопый макрос, не использования ради, а забавы для.
    #define STR(WHAT) ({std::stringstream e;e<<WHAT;e.str();})

    использование:
    throw runtime_error(STR("2+2="<<5));
  • 0
    А такая же статья только форматированный ввод есть?

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.