c++stringstream

How to initialize a std::stringstream?


I need to concatenate a string with integers. To do that I'm using stringstream in the following way:

int numPeople = 10;
stringstream ss;
ss << "Number of people is " << numPeople;

And that worked. But I was trying to do it in the way below:

int numPeople = 10;
stringstream ss << "Number of people is " << numPeople;

And I was getting the following error: "expected initializer before '<<' token"

Why was I getting this error? Why can't I assign the stringstream value at the same time I declare it?


Solution

  • stringstream ss << "Number of people is " << numPeople;
    

    Why can't I assign the stringstream value at the same time I declare it?

    This is similar to hoping this would work...

    int x + 3 + 9;
    

    ...but this doesn't parse as a variable definition, let alone a definition and assignment.

    The legal way to define and initialise an object

    The only ways to define and initialise a variable with a non-default value are (in a grammar sense - this isn't code):

    type identifier(...args...);
    type identifier{...args...};
    type identifier = ...expression...;
    

    The last notation is equivalent to the first - i.e. type identifier(arg) where arg is passed the ...expression....

    Trying to use the legal notation for int and stringstream

    For int, you can easily correct the code:

    int x = 3 + 9;
    

    ...and it works because "3 + 9" can be evaluated independently first to give a sane value to store in x. The compiler's behaviour for operator + on ints does what we want: it produces the int result we then want to store in x. You can think of the above as:

            // evaluate the expression to assign first...
            ((int)3 + (int)9); // notate the implicit types
            (int)12;           // evaluate expression => value to assign
    int x = (int)12;           // then think about doing the assignment
    int x((int)12);            // construct a valid value
    

    It works! But if you try that for stringstream...

    stringstream ss = "Number of people is " << numPeople;  // BROKEN
                      "Number of people is " << numPeople; // bitshift?!
    

    ...it won't work, because "Number of people is " << numPeople needs to be evaluated first but is illegal - you'll get an error like:

    error C2296: '<<' : illegal, left operand has type 'const char [20]'
    

    The problem is that the compiler's still trying to apply the bitwise shift operation, which only makes sense for numbers, because the overloads for << that we want to use require that any "X << Y" code has the left-hand part "X" be - or be implicitly convertible to - an ostream&. A string literal can't be converted. At this point, the compiler is oblivious to the stringstream to which the result of the expression will be passed.

    A solution for stringstream

    It's a bit of a chicken-and-egg problem, because you need to combine the right-hand values you want in the stringstream to call the stringstream's constructor, but for that you need... a stringstream. You can actually pull that off with a temporary stringstream:

    static_cast<std::ostringstream&&>(std::ostringstream{} << "Number of people is " << numPeople)
    

    The cast is unfortunately needed because the operator<< overloads handle stringstreams via references to their ostream base class, returning an ostream&, so you need to cast back to the stringstream type manually, so you can then invoke the std::stringstream move constructor...

    The complete one-liner construction is then...

    std::ostringstream ss(static_cast<std::ostringstream&&>(std::ostringstream{} << "Number of people is " << numPeople));
    ...or...
    auto&& ss = static_cast<std::ostringstream&&>(std::ostringstream{} << "Number of people is " << numPeople);
    

    ...but that's too hideous to contemplate.

    Making the solution (arguably) less hideous...

    ...with a wrapper object

    You can use an object to forward streaming operations to an ostringstream whilst ignoring the returned std::ostream& or && and instead returning a reference to the derived type:

    struct iloss : private std::ostringstream {
        iloss&& operator<<(const auto& x) && {
            static_cast<std::ostringstream&&>(*this) << x;
            return std::move(*this);
        }
        iloss& operator<<(const auto& x) & {
            static_cast<std::ostringstream&>(*this) << x;
            return *this;
        }
        using std::ostringstream::str; 
    };
    

    That lets you write code like:

    std::string x = (iloss{} << "pi~=" << std::fixed << std::setprecision(6) << std::numbers::pi).str();
    

    ...with macros

    Yes, you read that right. Depending on your sensibilities, you may feel a macro helps or is worse...

    #define OSS(VALUES) \
        static_cast<std::ostringstream&&>(std::ostringstream{} << VALUES)
    
    auto&& ss = OSS("Number of people is " << numPeople);
    

    FWIW, you could also use the macro to create strings...

    auto&& s = OSS("Number of people is " << numPeople).str(); 
    

    ...or create a dedicated macro...

    #define STR(VALUES) \
        static_cast<std::ostringstream&&>(std::ostringstream{} << VALUES).str()
    auto&& s = STR("Number of people is " << numPeople);
    

    An (arguably) better practice - separate construction and initialisation

    Just create the stringstream - optionally providing a single string to the constructor - then use operator<< in a second statement:

    std::stringstream ss;
    ss << "Number of people is " << numPeople;
    

    This is much easier to read, and there are no weird macros required.

    An alternative

    C++11 introduced to_string() overloads which are convenient if you have an integral value or two to concatentate with or into a string:

    auto&& s = "Number of people is " + std::to_string(numPeople);
    

    This may be inefficient though (check your compiler(s) optimisation abilities if you care): each std::to_string() is likely to dynamically allocate a buffer for an independent std::string instance, then the individual concatenations may involve extra copying of text, and the original dynamically-allocated buffers may need to be enlarged, then most of those temporary std::strings will take time to deallocate during destruction.

    Discussion

    Ideally, std::stringstream would have a constructor accepting an arbitrary number of constructor arguments (A, B, C...) to be formatted into the stringstream as if by a subsequent << A << B << C.... There are already constructors with arguments (e.g. (std::ios_base::openmode, const Allocator&)), so we'd need a placeholder to distinguish such arguments from values we're trying to format into the stream, or a weirder workaround like requiring the values to be formatted into the stream be passed in as an initialiser list.

    Still, it looks and feels very weird using strings with , instead of <<:

    std::stringstream ss{"n:", std::setw(4), std::hex, '\n'};
    

    And then if during code maintenance you find you need to move the streaming values to a point after construction, you'd need to change the separator. Breaking it out into two lines to start with - construction then streaming - simplifies that maintenance.

    It was worse in C++03

    C++03 lacked move constructors, so it was necessary to use the std::ostringstream::str() member function on the temporary to get an extra deep-copy of the std::string with which to construct the named stringsteam...

    stringstream ss(static_cast<std::ostringstream&>(std::ostringstream() << "Number of people is " << numPeople).str());
    

    With this C++03 code, there's a likelihood of duplicate dynamic memory allocations (unless the strings are short enough to fit inside the string object, a commonly provided std::string technique called "Short String Optimisation" or SSO). There's also a deep copy of textual content. Construction-followed-by-streaming was a better approach.