Избегайте копирования строки из ostringstream

У меня есть имя std::string, которое я хочу заполнить данными через интерфейс std::ostream и избежать копирования строки.
Один из способов сделать это, который требует копирования, заключается в следующем:

bool f(std::string& out)
{
   std::ostringstream ostr;
   fillWithData(ostr);
   out = ostr.str(); // 2 copies here
   return true;
}

Мне нужно передать результат через out, и я не могу вернуть ostr.str().
Я хочу избежать копий в out = ostr.str();, так как эта строка может быть очень большой.

Есть ли способ, возможно, с помощью rdbuf()s, связать буфер std::ostream напрямую с out?

Чтобы уточнить, меня интересует автоматическое расширение std::string и std::ostream, чтобы вызывающему абоненту не нужно было знать размер перед вызовом.

ОБНОВЛЕНИЕ: я только что понял, что безобидная строка out = ostr.str();, вероятно, повлечет за собой 2 копии:

  1. Первый по вызову str()
  2. Другой с помощью оператора присваивания std::string.

person Adi Shavit    schedule 30.04.2014    source источник
comment
Не выглядит возможным. Может ли ваша функция f принимать std::ostringstream & в качестве аргумента?   -  person Didier Trosset    schedule 30.04.2014
comment
Проблема с тем, что вы хотите сделать, заключается в том, что стандарт C++ не требует, чтобы внутренний буфер std::string_buf был std::string.   -  person indeterminately sequenced    schedule 30.04.2014
comment
@DidierTrosset: Не совсем так. Внутреннее использование std::ostringstream — это деталь реализации.   -  person Adi Shavit    schedule 30.04.2014
comment
Примечание. Вторая копия (назначение), скорее всего, оптимизирована (проблем быть не должно).   -  person    schedule 01.05.2014
comment
Аналогичная тема: stackoverflow.com/ вопросы/26266525/   -  person M.M    schedule 30.07.2018
comment
Второго экземпляра нет. В каждом достойном компиляторе эта копия пропущена. Начиная с C++11, даже если это не так, строка перемещается. Начиная с C++17, копия пропускается. ostringstream не поддерживает создание строки с повторным использованием ее памяти, не с текущим стандартом.   -  person ytoledano    schedule 25.06.2019


Ответы (2)


Напишите свой собственный поток:

#include <ostream>

template <typename Char, typename Traits = std::char_traits<Char>>
class BasicStringOutputBuffer : public std::basic_streambuf<Char, Traits>
{
    // Types
    // =====

    private:
    typedef std::basic_streambuf<Char, Traits> Base;

    public:
    typedef typename Base::char_type char_type;
    typedef typename Base::int_type int_type;
    typedef typename Base::pos_type pos_type;
    typedef typename Base::off_type off_type;
    typedef typename Base::traits_type traits_type;

    typedef typename std::basic_string<char_type> string_type;

    // Element Access
    // ==============

    public:
    const string_type& str() const  { return m_str; }
    string_type& str() { return m_str; }

    // Stream Buffer Interface
    // =======================

    protected:
    virtual std::streamsize xsputn(const char_type* s, std::streamsize n);
    virtual int_type overflow(int_type);

    // Utilities
    // =========

    protected:
    int_type eof() { return traits_type::eof(); }
    bool is_eof(int_type ch) { return ch == eof(); }

    private:
    string_type m_str;
};

// Put Area
// ========

template < typename Char, typename Traits>
std::streamsize
BasicStringOutputBuffer<Char, Traits>::xsputn(const char_type* s, std::streamsize n) {
    m_str.append(s, n);
    return n;
}

template < typename Char, typename Traits>
typename BasicStringOutputBuffer<Char, Traits>::int_type
BasicStringOutputBuffer<Char, Traits>::overflow(int_type ch)
{
    if(is_eof(ch)) return eof();
    else {
        char_type c = traits_type::to_char_type(ch);
        return xsputn(&c, 1);
    }
}


// BasicStringOutputStream
//=============================================================================

template < typename Char, typename Traits = std::char_traits<Char> >
class BasicStringOutputStream : public std::basic_ostream<Char, Traits>
{
    protected:
    typedef std::basic_ostream<Char, Traits> Base;

    public:
    typedef typename Base::char_type char_type;
    typedef typename Base::int_type int_type;
    typedef typename Base::pos_type pos_type;
    typedef typename Base::off_type off_type;
    typedef typename Base::traits_type traits_type;
    typedef typename BasicStringOutputBuffer<Char, Traits>::string_type string_type;

    // Construction
    // ============

    public:
    BasicStringOutputStream()
    :   Base(&m_buf)
    {}

    // Element Access
    // ==============

    public:
    const string_type& str() const { return m_buf.str(); }
    string_type& str() { return m_buf.str(); }

    private:
    BasicStringOutputBuffer<Char, Traits> m_buf;
};

typedef BasicStringOutputStream<char> StringOutputStream;


// Test
// ====

#include <iostream>

int main() {
    StringOutputStream stream;
    stream << "The answer is " << 42;
    std::string result;
    result.swap(stream.str());
    std::cout << result << '\n';

}

Примечание. Вы можете управлять указателями области размещения в более сложной реализации.

person Community    schedule 30.04.2014
comment
+1 за комплексное решение. Не сделают ли Boost iostreams код короче и менее подверженным ошибкам? - person Adi Shavit; 01.05.2014
comment
@AdiShavit Я думаю, что boost::iostreams — это удобные оболочки, не предоставляющие дополнительных функций (но я могу ошибаться) - person ; 01.05.2014

Вот мое собственное решение для буфера потока из https://stackoverflow.com/a/51571896/577234. Он намного короче, чем у Дитера — нужно только реализовать overflow(). Он также имеет лучшую производительность для повторяющихся ostream::put() за счет настройки буферов. Производительность для больших операций записи с использованием ostream::write() будет такой же, поскольку она вызывает xsputn() вместо overflow().

class MemoryOutputStreamBuffer : public streambuf
{
public:
    MemoryOutputStreamBuffer(vector<uint8_t> &b) : buffer(b)
    {
    }
    int_type overflow(int_type c)
    {
        size_t size = this->size();   // can be > oldCapacity due to seeking past end
        size_t oldCapacity = buffer.size();

        size_t newCapacity = max(oldCapacity + 100, size * 2);
        buffer.resize(newCapacity);

        char *b = (char *)&buffer[0];
        setp(b, &b[newCapacity]);
        pbump(size);
        if (c != EOF)
        {
            buffer[size] = c;
            pbump(1);
        }
        return c;
    }
  #ifdef ALLOW_MEM_OUT_STREAM_RANDOM_ACCESS
    streampos MemoryOutputStreamBuffer::seekpos(streampos pos,
                                                ios_base::openmode which)
    {
        setp(pbase(), epptr());
        pbump(pos);
        // GCC's streambuf doesn't allow put pointer to go out of bounds or else xsputn() will have integer overflow
        // Microsoft's does allow out of bounds, so manually calling overflow() isn't needed
        if (pptr() > epptr())
            overflow(EOF);
        return pos;
    }
    // redundant, but necessary for tellp() to work
    // https://stackoverflow.com/questions/29132458/why-does-the-standard-have-both-seekpos-and-seekoff
    streampos MemoryOutputStreamBuffer::seekoff(streamoff offset,
                                                ios_base::seekdir way,
                                                ios_base::openmode which)
    {
        streampos pos;
        switch (way)
        {
        case ios_base::beg:
            pos = offset;
            break;
        case ios_base::cur:
            pos = (pptr() - pbase()) + offset;
            break;
        case ios_base::end:
            pos = (epptr() - pbase()) + offset;
            break;
        }
        return seekpos(pos, which);
    }
#endif    
    size_t size()
    {
        return pptr() - pbase();
    }
private:
    std::vector<uint8_t> &buffer;
};

Говорят, что хороший программист — ленивый, поэтому вот альтернативная реализация, которую я придумал, для которой требуется еще меньше пользовательского кода. Однако существует риск утечки памяти, поскольку он захватывает буфер внутри MyStringBuffer, но не освобождает MyStringBuffer. На практике для streambuf GCC не происходит утечки, что я подтвердил с помощью AddressSanitizer.

class MyStringBuffer : public stringbuf
{
public:
  uint8_t &operator[](size_t index)
  {
    uint8_t *b = (uint8_t *)pbase();
    return b[index];
  }
  size_t size()
  {
    return pptr() - pbase();
  }
};

// caller is responsible for freeing out
void Test(uint8_t *&_out, size_t &size)
{
  uint8_t dummy[sizeof(MyStringBuffer)];
  new (dummy) MyStringBuffer;  // construct MyStringBuffer using existing memory

  MyStringBuffer &buf = *(MyStringBuffer *)dummy;
  ostream out(&buf);

  out << "hello world";
  _out = &buf[0];
  size = buf.size();
}
person Yale Zhang    schedule 30.07.2018