Skip to main content
GameDev.net gamedev.net
🔒 Locked

const char * vs const std::string& in parameters

Started by all_names_taken Sep 30, 2008 at 2:32 PM 22 replies 14.4k views
Original Post
all_names_taken
all_names_taken
If I pass a constant string literal to a function, and it takes an std::string, will that create a temporary std::string object with the cost of a memory allocation and release by new and delete respectively, as well as the cost of a copy operation? Example:

void f(const std::string& s) {
  std::cout << s;
}

int main() {
  f("Hello world!");
}


If the parameter is declared as a const char *, it's obvious that there will be no such memory allocation, and if I really need to pass an std::string I can do so with c_str(). Using std::string as parameter will, by a plain translation, result in a copy operation, but a clever compiler should optimize that away in my opinion. Are compilers that clever, or is const char * a better alternative if I need to be absolutely sure there's no unnecessary copying and allocation taking place? How can I find out? I'm using Visual C++ and GCC. Note I may be using more complex indirect passing of the parameter through several nested function calls etc., and another interesting case is if I use an old API which returns a dynamically allocated char * and I need to pass it to a method for reading. In which cases, if any, will using std::string as parameter lead to invocation of new and delete calls which can't be optimized away by the compiler?
Antheus
Antheus
Quote:
Original post by all_names_taken
If I pass a constant string literal to a function, and it takes an std::string, will that create a temporary std::string object with the cost of a memory allocation and release by new and delete respectively, as well as the cost of a copy operation?


Std::string will be auto-allocated, the contents will be dynamically allocated from heap using new/delete or whatever implementation uses.

Passing a const char * will not result in copy of std::string object, but in construction of one.

Under rare circumstances, the construction of std::string may be omitted, but that's not the case with implementations I'm aware of, which all result in construction of a single std::string in optimized mode, and copy construction from temporary without optimization.

Quote:
will using std::string as parameter lead to invocation of new and delete calls which can't be optimized away by the compiler?


Auto-allocated objects are never allocated via new/delete. They reside on stack. Contents of std::string may be allocated via new/delete, including a copy of contents of original string, depending on implementation (MVC's version has an internal stack-allocated buffer for short strings, which avoids that).

Quote:
How can I find out?


Look at generated assembly.
Anon Mike
Anon Mike
I would be very surprised if the string conversion was optimized away in anything except the most trival cases and maybe not even then. std::string is not a built-in type so the compiler can't make any assumptions about it's behavior. There may also not be any new/delete calls at all - a common optimization on string classes is to have a small char array embedded in the class that is used for small strings. But then you (may) get memcpy's instead.

In short, there are no guarantees. If fine-grained control of allocations patterns is really that important to you then you'll have to do it yourself. I don't know your scenario, but in the vast majority of cases the extra cost of design and maintenance of the code plus the increased likelyhood of bugs doesn't justify the effort IMHO.
-Mike
Yann L
Yann L
Quote:
Original post by all_names_taken
Are compilers that clever, or is const char * a better alternative if I need to be absolutely sure there's no unnecessary copying and allocation taking place?

Why do have to be absolutely sure ? How can you know that this will be a problem without having done any profiling ?
alvaro
alvaro
There is a valid reason for taking a char const *' instead of a std::string const &', but it has nothing to do with performance. You don't want to impose on all your users to have to know about std::string just to be able to call your function. Keeping dependencies down is generally a good thing. If the caller happens to have a std::string, it's easy enough to call c_str() on it when calling your function.

You can find examples of this in the standard library. For instance, std::ifstream has a constructor that takes a filename as a `char const *'. By following the same convention, you know that every C++ programmer will know how to use your function.
loufoque
loufoque
There is a need for a string_ref type that would solve the issue.

The same problem happens if you take a vector. What if your memory actually lies in an array on the stack or in the data segment?
The solution is to take a range, which even allows it to work with other data structures.
But in the case of strings, it's a bit overkill.
Sc4Freak
Sc4Freak
It may be worth noting that some implementations of std::string contain a small statically allocated buffer for storing small strings without the need for dynamic allocation. In MSVC, I believe the size of this buffer is 16 characters. So in your example of "Hello world!", no memory allocation will take place (but there'll still be a 12-byte copy operation involved).
Bregma
Bregma
Quote:
Original post by alvaro
There is a valid reason for taking a char const *' instead of a std::string const &', but it has nothing to do with performance. You don't want to impose on all your users to have to know about std::string just to be able to call your function. Keeping dependencies down is generally a good thing. If the caller happens to have a std::string, it's easy enough to call c_str() on it when calling your function.

Well, I would argue the opposite: you should assume that a C++ programmer is familiar with the basics of the language, such as the use of std::string. You should not rely on their knowledge of obscure corner cases such as the use of pointers to char provided for backwards compatibility with a 40-year-old language like C.

Using char* instead of const std::string& has in the past been shown to be an excellent way to squeeze out a few percent better performance in certain tight situations (cf.Bulka and Mayhew for an excellent treatment of this subject). To address this, most C++ standard library implementors have been adopting the SSO (short string optimization) implementation of std::string, which provides execellent performance characteristics for most situations.
Quote:

You can find examples of this in the standard library. For instance, std::ifstream has a constructor that takes a filename as a `char const *'.

Er, yes, that oversight/bug has been fixed in C++09.
Stephen M. Webb
Professional Free Software Developer
all_names_taken
all_names_taken
Quote:
Original post by Yann L
Quote:
Original post by all_names_taken
Are compilers that clever, or is const char * a better alternative if I need to be absolutely sure there's no unnecessary copying and allocation taking place?

Why do have to be absolutely sure ? How can you know that this will be a problem without having done any profiling ?

I'm going to be using the pattern I choose here almost everywhere in all code I write. I won't know how much it matters until I have profiled it, but it'll be a heck of a job to replace this stuff everywhere if I made the wrong choice... A reason why I just don't choose const std::string& right away is that IMO there aren't too big syntactic disadvantages to using const char *, as it's pretty easy to write .c_str() when calling the method with a const std::string&. Taking an std::string as argument is of course slightly more convenient as both const char * and const std::string& will be accepted (with automatic conversion for the former), but as I said, I don't think typing c_str() is that horrible, in particular for methods whose calls I'm pretty sure 90-100% of the time will be made with a const char * and not an std::string. On the other hand, when the method takes std::string more than say 30% of the time I tend to favor const std::string& as parameter. The question is whether std::string creation is optimized away effectively enough for me to be able to choose to use std::string consistently.

Anyhow, the short string optimization sounds good, with that I can just keep in mind to try and choose short enough strings for constant string cases (the memcpy isn't too bad).

About the new and delete costs, how much do they really cost on a modern system? I've just been told they're extremely expensive and should be avoided at all costs as often as possible and this is what I typically try to do. Usually it hasn't hurt design or readability to do so, until I thought of this issue with string literals. How evil (or harmless) are they really and what would be a good way to find out? Can profilers be used to find out? How much is it worth to eliminate new/delete calls compared to say arithmetic or memory access instructions? From what I understand they're implemented as system calls, and so depend on operating system, right?

Also, how do I do to see compiler-generated assembly?

[Edited by - all_names_taken on October 1, 2008 1:48:16 PM]
Antheus
Antheus
Quote:
Original post by all_names_taken

About the new and delete costs, how much do they really cost on a modern system? I've just been told they're extremely expensive and should be avoided at all costs as often as possible and this is what I typically try to do.


My car gets 1 mile per gallon. Since I don't drive it, it costs me nothing, making it incredibly efficient.

Quote:
How evil (or harmless) are they really?


What are you doing with them? If your strings are declared at compile-time as const char *, and you never modify them, then frequently, char * is optimal and use of std::string is redundant.

Quote:
How much is it worth to eliminate new/delete calls compared to say arithmetic or memory access instructions?


That question would classify as weasel question if such a thing existed.

The question is: do you need to allocate strings (arbitrary non-constant length) dynamically or are they known at compile time. Further, what type of operations will you be performing on them.

If they are not known at compile time, can you define maximum length that a string can be (careful! - assumptions are cause of many failures), and if so, can you claim this is acceptable overhead (string will be 80 chars most, but you need to allocate 1 million instances of Hello World!, having overhead factor of 700%, whereas std::string would be about 40%).

What will be the life-cycle of strings. Can they exist on stack only? Or do they need to pass stack boundaries?

Quote:
From what I understand they're implemented as system calls, and so depend on operating system, right?


Not really. That is - new and delete do not result in a system call directly.


All the questions asked are design/logic problems, not compiler or language specific problems.

For example, one argument for globals is the lack of need to pass and store references around, but conveniently forgetting that the value is simply hard-coded - which is a design issue. Should such value be hard-coded?

The reason for using C or C++ vs. managed languages is the very memory management itself. But that question is mostly unrelated to std::XXX clases, it's often not addressed properly during design of an algorithm.



And again - if your application is really that performance sensitive, then looking at generated assembly will be your best bet. String literals may be costlier to use due to need to dynamically calculate the length.

Quote:
Note I may be using more complex indirect passing of the parameter through several nested function calls


Obviously, as long as you're passing by reference, object will be constructed once, then the reference will be passed.

Quote:
Also, how do I do to see compiler-generated assembly?


MVC is quite convenient for this, just put a breakpoint and hit alt-8.
all_names_taken
all_names_taken
Quote:
Original post by Antheus
Quote:
How evil (or harmless) are they really?


What are you doing with them? If your strings are declared at compile-time as const char *, and you never modify them, then frequently, char * is optimal and use of std::string is redundant.

Ok, that's what I was thinking.

Quote:
Original post by Antheus
Quote:
Note I may be using more complex indirect passing of the parameter through several nested function calls


Obviously, as long as you're passing by reference, object will be constructed once, then the reference will be passed.

Hm yeah, I guess things would be really bad though if I switched from const char * to string back and forth :). That would create a new string many times :). So if I am to use const char * I must be sure it's "at the bottom layer" of the app so to speak (that it won't internally call a method that takes std::string).

Quote:
Original post by Antheus
MVC is quite convenient for this, just put a breakpoint and hit alt-8.

Ok, thanks and rate++!


Quote:
Original post by bubu LV
I know that there is const_string: http://conststring.sourceforge.net/

Looks interesting, thanks and rate++!
Yann L
Yann L
Could you be a bit more precise on what exactly you are going to do with these functions, ie. their intended usage profile ? From the (limited) information I gathered in this thread so far, I think that you are thinking very hard on solving a problem that doesn't even exist. I'd dare to say that for 99.9% of all real world applications out there, the difference between a char pointer and a string ref would be completely insignificant. And by only providing a char * interface, you also give up many things - reduced type safety, more cludgy syntax, potential conversion overhead (c_str is not free either), potential issues with thread safety, etc.

While I'm something of an optimization nut myself, frankly, I think this is a textbook case of premature optimization.
loufoque
loufoque
No implementation can reasonably not make c_str free.
The next standard is going to mandate std::string be contiguous anyway.

Quote:
the difference between a char pointer and a string ref would be completely insignificant.

No, it wouldn't.
One would be a sequence of characters, the other would be a pointer to a char.

A string_ref would know its size, and provide const string methods.
Yann L
Yann L
Quote:
Original post by loufoque
No implementation can reasonably not make c_str free.

It will never be entirely free, especially in combination with short string optimization. And currently, afaik, there is nothing forcing an implementation to keep string memory contiguous. In the worst case scenario, c_str could even trigger an internal copy to a temporary buffer. It will most likely not happen with a sane implementation, but there is no guarantee.

And a null terminator, which is not used on most string implementations I'm aware of, also has to be added.

Quote:
Original post by loufoque
No, it wouldn't.
One would be a sequence of characters, the other would be a pointer to a char.

The performance difference would be insignificant. And when saying 'string ref' I was referring to a std::string&

This is just like all other micro-optimizations. Never assume you know how it will react under some scenario beforehand. Profile it. And as I said above, I guarantee you that in the vast majority of real world applications, switching from const std::string& to a char pointer argument will gain you absolutely nothing in terms of speed.

Seriously, if you're concerned with the speed of strings, then you'd better start by supplying a custom allocator. This will give you a much, much larger performance boost for all std containers than some string vs. raw pointer thing.
ChaosEngine
ChaosEngine
Quote:
Original post by all_names_taken
I'm going to be using the pattern I choose here almost everywhere in all code I write.


Why? Seriously, what's wrong with having some functions take a const char* (i.e. functions that do no string manipulation inside them and are performance critical) and some take a const std::string& (i.e. functions that maybe need to do some string manipulation and funcations that will typically be passed an std::string anyway).

To be honest, I'd lean towards the const std::string& version. I assume the majority of your strings within your app are going to be std::strings, so the overhead will be close to zero. Write your app with the convenience of a const std::string and then profile it later to see if any of those functions are bottlenecks. It's highly unlikely that the creation of an std::string on the stack will be any kind of significant overhead in all but the most trivial of functions and on the remote chance that you do come across that situation, fine, change that param to a const char* and away you go. Worst case scenario, you have to add ".c_str()" where ever you call that function with an std::string, (which the compiler will identify for you and using any reasonable IDE should be about 30 mins work at most to replace).



if you think programming is like sex, you probably haven't done much of either.-------------- - capn_midnight
all_names_taken
all_names_taken
I think this has been an interesting discussion - it has finally, after some thinking, convinced me to abandon const char * for most cases, if not all. The more I think about it, the more I tend to favor using std::string, not only because it's cleaner, but also because const char * gets WORSE performance in a not too uncommon case: if the method ever needs to internally call any STL or other method that takes an std::string. For example:
void f(const char *p) { g(p); /*creates a second string for the method call*/ }void g(const std::string& s) { std::cout << s; }void main() {  std::string foo("Hello"); /*obviously creates a string*/  f(foo);}

Versus:
void f(const std::string& s) { g(p); }void g(const std::string& s) { std::cout << s; }void main() {  std::string foo("Hello"); /*obviously creates a string*/  f(foo);}

So, in this case, using const char * means creating 2 strings (and their constructors/destructors invokes expensive new/delete calls), whereas using const std::string& everywhere would mean creating only 1 string.

This example, in my opinion, suggests that if you don't want to use const std::string& EVERYWHERE, you're forced to apply the following guideline:

if function f takes a const char *, then all functions called by f must take const char * (or we must write custom versions of those functions for taking a const char * as argument)

The whole-program architecture consequences of this would be that you either get bloating with const char * versions of most low level library functions, or that you'd tend to make all your low level library/API functions take const char *, since they're likely to later be called by higher level functions that wanted to take const char *. That can probably get pretty annoying, as it forces the developer to trace downwards in the code to ensure there are no const string&->const char*->const string& sequences generated by the possible call graph, which makes it difficult to have developers work in different abstraction layers within a project: they need to know what's hidden underneath, thus violating encapsulation.

So I guess using std::string EVERYWHERE isn't too bad after all :D
loufoque
loufoque
Quote:
It will never be entirely free, especially in combination with short string optimization. And currently, afaik, there is nothing forcing an implementation to keep string memory contiguous. In the worst case scenario, c_str could even trigger an internal copy to a temporary buffer. It will most likely not happen with a sane implementation, but there is no guarantee.

And a null terminator, which is not used on most string implementations I'm aware of, also has to be added.

Whether SBO is used or not doesn't change anything. There is no copy to be done. If the buffer in the object is used, c_str returns a pointer to that. If it's not, then dynamically allocated memory is used, and c_str returns a pointer to that.
Implementations that make use of SBO don't play at putting parts of the string in the in-object buffer if it doesn't fit.

Also, all recent implementations I know of allocate an extra null character at the end just so that it can directly be used with c_str.
Yann L
Yann L
Quote:
Original post by loufoque
Whether SBO is used or not doesn't change anything. There is no copy to be done. If the buffer in the object is used, c_str returns a pointer to that. If it's not, then dynamically allocated memory is used, and c_str returns a pointer to that.

I think you didn't read at all what I said. I said that there is no guarantee that contiguous memory is used. In which case, a copy is obviously unavoidable. If small buffer optimization is used, then there is still an overhead of at least one comparison and branching instruction, since the implementation needs to decide which pointer to return. You said earlier that c_str() would be "free". It certainly isn't.

Quote:
Original post by loufoque
Also, all recent implementations I know of allocate an extra null character at the end just so that it can directly be used with c_str.

Again, this is not what I said. There is obviously no reallocation taking place, but the null char has to be set anyway. No sane implementation will update the terminator on each string operation. So it has to do so in the c_str call. This again is at least a memory dereference and a write access. Once more, c_str is certainly not "free".

"Free" would be a simple return of a pointer. c_str does a lot more than that. Not that it really matters from an optimization point of view, but saying that this operation wouldn't have any overhead is just plain incorrect.
SiCrane
SiCrane
Actually, you only need to update the null terminator for mutating operations that resize the string, so c_str() generally doesn't muck with the null terminator in most standard library implementations.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.