1

I have the following mock up code of a class which uses an attribute to set a filename:

#include <iostream>
#include <iomanip>
#include <sstream>

class Test {
    public:
        Test() { id_ = 1; }
        /* Code which modifies ID */

        void save() {
            std::string filename ("file_");
            filename += getID();
            std::cout << "Saving into: " << filename <<'\n';
        }

    private:
         const std::string getID() {
             std::ostringstream oss;
             oss << std::setw(4) << std::setfill('0') << id_;
             return oss.str();
         }

         int id_;
};

int main () {
    Test t;
    t.save();
}

My concern is about the getID method. At first sight it seems pretty inefficient since I am creating the ostringstream and its corresponding string to return. My questions:

1) Since it returns const std::string is the compiler (GCC in my case) able to optimize it?

2) Is there any way to improve the performance of the code? Maybe move semantics or something like that?

Thank you!

Javi
  • 3,440
  • 5
  • 29
  • 43
  • Have you measured it? Actually opening a file and writing to it is likely to be 10-100x more expensive... – Chris Dodd Jan 30 '15 at 15:29
  • I didn't. I know the time spent on these operations is negligible. However, I'm wondering it in order to learn how to properly write these kind of code. – Javi Jan 30 '15 at 15:35
  • 1
    If that's the only place `getId()` is being used, you have absolutely nothing to worry about. You're doing this properly. Even if it's used in other places, you're still fine. Unless it's being called in a tight loop and you've `profiled` it and have determined that it is too expensive, you shouldn't need to complicate your code with strategies such as caching. – Julian Jan 30 '15 at 16:34
  • 2
    FYI returning by const value is a strong pessimization and also always inappropriate from a contractual design standpoint. Don't tell the caller of a function what they can or can't do with what you returned to them. That's theirs now. If they want it to be `const` they can store it as such. (const T& is good, const T* is good, const T* const is BAD, const T is BAD) – David Feb 01 '15 at 05:45
  • Thank you for the info. I returned `const` because I cannot return `const T&` and I thought returning `const` value would help the compiler to optimize so that it avoids value copying. – Javi Feb 01 '15 at 21:50
  • If you really hit stringstream performance problems make it static. See also: http://cplusplus.bordoon.com/speeding_up_string_conversions.html – Alexander Oh Feb 05 '15 at 10:48

4 Answers4

1

1) Since it returns const std::string is the compiler (GCC in my case) able to optimize it?

Makes no sense to return a const object unless returning by reference

2) Is there any way to improve the performance of the code? Maybe move semantics or something like that?

Id id_ does not change, just create the value in the constructor, using an static method may help:

     static std::string format_id(int id) {
         std::ostringstream oss;
         oss << std::setw(4) << std::setfill('0') << id;
         return oss.str();
     }

And then:

Test::Test()
 : id_(1)
 , id_str_(format_id(id_))
{ }

Update:

This answer is not totally valid for the problem due to the fact that id_ does change, I will not remove it 'cause maybe someone will find it usefull for his case. Anyway, I wanted to clarify some thoughts:

  • Must be static in order to be used in variable initialization
  • There was a mistake in the code (now corrected), which used the member variable id_.
  • It makes no sense to return a const object by value, because returning by value will just copy (ignoring optimizations) the result to a new variable, which is in the scope of the caller (and might be not const).

My advice

An option is to update the id_str_ field anytime id_ changes (you must have a setter for id_), given that you're already changin the member id_ I assume there will be no issues updating another.

This approach allows to implement getID() as a simple getter (should be const, btw) with no performance issues, and the string field is computed only once.

ichramm
  • 6,437
  • 19
  • 30
  • The value of `id_` actually changes (the comment is maybe not explanatory-enough). This is a mock-up code, there are other functions in reality which modify the id_`. – Javi Jan 30 '15 at 15:36
  • How does making that function static help? – Javi Jan 30 '15 at 15:46
  • 1
    A `static` method would not be able to access `this->id_` – Drew Dormann Jan 30 '15 at 15:46
  • 1
    **First.** "Makes no sense to return a const object unless returning by reference". What about this (dummy) code: `getID() = "blabla";` ? **Second** It make sense to make **stringstream** static, but not `getID` method – borisbn Jan 30 '15 at 16:10
  • @borisbn `getID() = "blabla";` won't compile, `unless returning by reference`, as ichramm has already mentioned. – Julian Jan 30 '15 at 16:26
  • @AtlasC1 look at this - http://ideone.com/u3Lz9M - I mean that it `make` sense to add `const` when you want to return the object by value – borisbn Jan 31 '15 at 09:11
  • 1
    @borisbn Returning by const value will disable move semantics, which is not what you usually want. If you really want to prevent `foo() = "bar"` on your own types, you could define `Foo& operator=(const Foo&) const&& = delete;`. I'm not saying that's a good idea; just saying that you *could*. – Quuxplusone Feb 02 '15 at 19:17
1

Creating an ostringstream, just once, prior to an expensive operation like opening a file, doesn't matter to your program's efficiency at all, so don't worry about it.

However, you should worry about one bad habit exhibited in your code. To your credit, you seem to have identified it already:

1) Since it returns const std::string is the compiler (GCC in my case) able to optimize it?

2) Is there any way to improve the performance of the code? Maybe move semantics or something like that?

Yes. Consider:

class Test {
    // ...
    const std::string getID();
};

int main() {
    std::string x;
    Test t;
    x = t.getID();  // HERE
}

On the line marked // HERE, which assignment operator is called? We want to call the move assignment operator, but that operator is prototyped as

string& operator=(string&&);

and the argument we're actually passing to our operator= is of type "reference to an rvalue of type const string" — i.e., const string&&. The rules of const-correctness prevent us from silently converting that const string&& to a string&&, so when the compiler is creating the set of assignment-operator functions it's possible to use here (the overload set), it must exclude the move-assignment operator that takes string&&.

Therefore, x = t.getID(); ends up calling the copy-assignment operator (since const string&& can safely be converted to const string&), and you make an extra copy that could have been avoided if only you hadn't gotten into the bad habit of const-qualifying your return types.


Also, of course, the getID() member function should probably be declared as const, because it doesn't need to modify the *this object.

So the proper prototype is:

class Test {
    // ...
    std::string getID() const;
};

The rule of thumb is: Always return by value, and never return by const value.

Quuxplusone
  • 23,928
  • 8
  • 94
  • 159
  • Thank you for the elaborated answer. Do you mean that by just changing the function prototype the compiler will be able to use `string& operator=(string&&);`? – Javi Feb 03 '15 at 09:51
  • Yes, that's correct. Assigning from a non-const return value is usually more efficient than assigning from a const return value, simply because non-const values can be modified (e.g., they can have their guts stolen by a move assignment operator). Removing the `const` keyword and recompiling will automagically speed up your code. – Quuxplusone Feb 03 '15 at 19:43
0

You could change getID in this way:

std::string getID() {
  thread_local std::ostringstream oss;
  oss.str("");  // replaces the input data with the given string
  oss.clear();  // resets the error flags

  oss << std::setw(4) << std::setfill('0') << id_;
  return oss.str();
}

it won't create a new ostringstream every single time.

In your case it isn't worth it (as Chris Dodd says opening a file and writing to it is likely to be 10-100x more expensive)... just to know.

Also consider that in any reasonable library implementation std::to_string will be at least as fast as stringstream.

1) Since it returns const std::string is the compiler (GCC in my case) able to optimize it?

There is a rationale for this practice, but it's essentially obsolete (e.g. Herb Sutter recommended returning const values for non-primitive types).

With C++11 it is strongly advised to return values as non-const so that you can take full advantage of rvalue references.

About this topic you can take a look at:

Community
  • 1
  • 1
manlio
  • 18,345
  • 14
  • 76
  • 126
  • Could I make `oss` a `Test` class member to make it more intuitive? The effect would be probably the same, wouldn't it? – Javi Jan 30 '15 at 15:49
  • I don't think it's more intuitive. It violates a good principle that you should limit variables scope to the minimum possible. You need `oss` only inside `getID`. – manlio Jan 30 '15 at 15:59
0

One possibility would be to do something like this:

std::string getID(int id) { 
    std::string ret(4, '0') = std::to_string(id);

    return ret.substring(ret.length()-4);
}

If you're using an implementation that includes the short string optimization (e.g., VC++) chances are pretty good that this will give a substantial speed improvement (a quick test with VC++ shows it at around 4-5 times as fast).

OTOH, if you're using an implementation that does not include short string optimization, chances are pretty good it'll be substantially slower. For example, running the same test with g++, produces code that's about 4-5 times slower.

One more point: if your ID number might be more than 4 digits long, this doesn't give the same behavior--it always returns a string of exactly 4 characters rather than the minimum of 4 created by the stringstream code. If your ID numbers may exceed 9999, then this code simply won't work for you.

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111