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'ом.

    Быть может, кому-то эта обёртка окажется полезной. Буду рад услышать комментарии, предложения и пожелания.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 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
              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
                            А такая же статья только форматированный ввод есть?

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