44

Consider this code snippet:

bool foo(const std::string& s) {
    return s == "hello"; // comparing against a const char* literal
}

bool bar(const std::string& s) {
    return s == "hello"s; // comparing against a std::string literal
}

At first sight, it looks like comparing against a const char* needs less assembly instructions1, as using a string literal will lead to an in-place construction of the std::string.

(EDIT: As pointed out in the answers, I forgot about the fact that effectively s.compare(const char*) will be called in foo(), so of course no in-place construction takes place in this case. Therefore striking out some lines below.)

However, looking at the operator==(const char*, const std::string&) reference:

All comparisons are done via the compare() member function.

From my understanding, this means that we will need to construct a std::string anyway in order to perform the comparison, so I suspect the overhead will be the same in the end (although hidden by the call to operator==).

  • Which of the comparisons should I prefer?
  • Does one version have advantages over the other (may be in specific situations)?

1 I'm aware that less assembly instructions doesn't neccessarily mean faster code, but I don't want to go into micro benchmarking here.

andreee
  • 3,626
  • 16
  • 33
  • 9
    `return s == "hello";`. – molbdnilo Jun 03 '19 at 12:48
  • 4
    better use `-O2/3` for the comparison, i mean who cares how many instructions it is in a debug build ? ;) – 463035818_is_not_a_number Jun 03 '19 at 12:48
  • 7
    @Someprogrammerdude No it isn't. It's a case of not writing needlessly wasteful code. The choice between a string literal and a needless string instantiation is not a micro optimisation; it's common sense! In other words, this _is_ about writing good code. – Lightness Races in Orbit Jun 03 '19 at 12:48
  • 1
    See overload (4) https://en.cppreference.com/w/cpp/string/basic_string/compare – Richard Critten Jun 03 '19 at 12:49
  • 9
    There is too much bloaty and slow code in the world at the moment because people consider writing good code that doesn't do more that it needs to, to be "premature optimisation" and have been scared away from thinking about the code they're writing as a result :( – Lightness Races in Orbit Jun 03 '19 at 12:51
  • 1
    `"hello"s` constructor calls `new`, it can throw. Making the strings a little bit longer, and you can see [the difference](https://godbolt.org/z/UWe2XX). – KamilCuk Jun 03 '19 at 12:52
  • 4
    @Someprogrammerdude this doesn't have _anything_ to do with premature optimization, I'm asking out of curiosity in the first place and also this would affect coding style to some extent. You don't tell people to pass `const A` vs. `const A&` in the general case just because they shouldn't care about "premature optimization" – andreee Jun 03 '19 at 12:53
  • @molbdnilo of course - I was so focused on the actual problem that I forgot about this :-) - fixed. – andreee Jun 03 '19 at 14:22

3 Answers3

67

Neither.

If you want to be clever, compare to "string"sv, which returns a std::string_view.


While comparing against a literal like "string" does not result in any allocation-overhead, it's treated as a null terminated string, with all the concomittant disadvantages: No tolerance for embedded nulls, and users must heed the null terminator.

"string"s does an allocation, barring small-string-optimisation or allocation elision. Also, the operator gets passed the length of the literal, no need to count, and it allows for embedded nulls.

And finally using "string"sv combines the advantages of both other approaches, avoiding their individual disadvantages. Also, a std::string_view is a far simpler beast than a std::string, especially if the latter uses SSO as all modern ones do.


At least since C++14 (which generally allowed eliding allocations), compilers could in theory optimise all options to the last one, given sufficient information (generally available for the example) and effort, under the as-if rule. We aren't there yet though.

Community
  • 1
  • 1
Deduplicator
  • 41,806
  • 6
  • 61
  • 104
  • the idea sound sane but there isn't a `std::string::compare(string_view ...)` function or `operator ==` defined between `std::string` and `std::string_view` – xception Jun 03 '19 at 12:52
  • 3
    @xception Yes there is. Unfortunately the standard talks about newer overloads like that in incredibly arcane terms for very little good reason (rather than the good old-fashioned method of just listing overloads and their simple arguments). But it's [7-9 here](https://en.cppreference.com/w/cpp/string/basic_string/compare) that you're looking for. – Lightness Races in Orbit Jun 03 '19 at 12:53
  • 5
    Sorry what's the benefit of this over a simple string literal? Don't get me wrong, I love string views and make judicious use of them when appropriate, but I'm not seeing it here. – Lightness Races in Orbit Jun 03 '19 at 12:55
  • @LightnessRacesinOrbit awesome, didn't know that, thanks for the info – xception Jun 03 '19 at 12:58
  • 1
    @LightnessRacesinOrbit We tried just saying `string_view`. Turns out that breaks existing code. – T.C. Jun 03 '19 at 12:58
  • 1
    @LightnessRacesinOrbit It can first compare the sizes, and than do a sized memory-comparison. That can be optimised better than always staying on the lookout for the terminator. Also, it allows embedded nulls if wanted. – Deduplicator Jun 03 '19 at 12:58
  • @Deduplicator Surely a sane implementation will just `strlen` up front? Which is effectively equivalent. And if the implementer decided that it's cheaper _not_ to `strlen` up front then they have the freedom to do so .. but they don't if you use a string_view. – Lightness Races in Orbit Jun 03 '19 at 12:59
  • 7
    in fact, with `-O3` gcc seems even to be able to [inline the call to `compare`](https://godbolt.org/z/pQ2r7m), which doesn't happen with the regular string literal. – Cássio Renan Jun 03 '19 at 13:00
  • 10
    @LightnessRacesinOrbit There is no `strlen()` for creating the string_view using `operator "" sv`. – Deduplicator Jun 03 '19 at 13:01
  • 1
    And I'd just gone and found https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/basic_string.tcc#L1429, too. Oh well. Can you add into your answer that the length can be baked into the SV construction at compile-time? I think that's the major key of your suggestion, and (assuming other people are stupid like me) it's not obvious from your wording. – Lightness Races in Orbit Jun 03 '19 at 13:02
  • 4
    Using string_view [looks](https://godbolt.org/z/jVUzSd) the fastest on `gcc9.1 -Ofast` because of inlining the call to compare. With `-Os` both string_view and string literal version call `memcmp`. – KamilCuk Jun 03 '19 at 13:03
  • Really nice answer, thanks! `std::string_view` really never crossed my mind in this case, but seems to be indeed an optimal solution! – andreee Jun 03 '19 at 13:27
  • [Inconsistent behavior of compiler optimization of unused string](//stackoverflow.com/q/56425276) has some details about optimizing away construction of a `std::string` with gcc vs. clang and libstdc++ vs. libc++. (clang can optimize away `delete new ...` but gcc can't, so only short `std::string` objects can optimize away.) – Peter Cordes Jun 04 '19 at 19:27
  • @PeterCordes I incorporated some more links and background info. Still, *all* the user-defined literals the standard library provides are in their respective namespace under std::literals, with all but std being inline-namespaces, [those for `std::string` are not exempt](http://coliru.stacked-crooked.com/a/b2b066f32c3fc1ef). – Deduplicator Jun 04 '19 at 20:29
  • Oops, I missed that the OP's godbolt link already included `using namespace std::string_literals;`. That makes a lot more sense. – Peter Cordes Jun 04 '19 at 20:33
14

No, compare() does not require construction of a std::string for const char* operands.

You're using overload #4 here.

The comparison to string literal is the "free" version you're looking for. Instantiating a std::string here is completely unnecessary.

Lightness Races in Orbit
  • 358,771
  • 68
  • 593
  • 989
  • 1
    "Free" only if the implementation of the suitable `compare` function doesn't itself create a `std::string` object (although that's probably very unlikely). – Some programmer dude Jun 03 '19 at 12:51
  • 5
    @Someprogrammerdude Free at this level of abstraction, yes. Theoretically the implementation could be designed to hive off the comparison to the cloud, resulting in multi-millisecond latency and potentially a login to Facebook - but let's be real ;) – Lightness Races in Orbit Jun 03 '19 at 12:52
  • (However that is indeed why I used quotes cos nothing's truly free really) – Lightness Races in Orbit Jun 03 '19 at 12:57
  • Hm, maybe I am missing/overlooking something here but how does it _not_ need to construct a `std::string` if `compare` is a member function (non-static)? – andreee Jun 03 '19 at 12:59
  • @andreee Because `s == "hello"` is equal to `s.operator==("hello")`. You already have the `std::string` object with `s`. – Some programmer dude Jun 03 '19 at 13:02
  • @andreee `s` is already a `std::string` object. The question is whether it can be compared against a character literal or only a `std::string`. – Martin Bonner supports Monica Jun 03 '19 at 13:02
  • Oh, my bad. I didn't consider `this` on the left hand side :-) However, what about the string literal case? Why do I get it "for free"? From what I understand, I am comparing _two_ `std::string`s (the one that is passed vs. the literal that needs to be constructed). Where is the trick? – andreee Jun 03 '19 at 13:26
  • I don't understand where you imagine the `std::string` construction to come from. You have a `std::string` on the left (already exists; it's yours) and a `const char*` on the right. That's it. The function compares the string with the C-string. That's it. – Lightness Races in Orbit Jun 03 '19 at 13:42
  • I might not be explaining it properly, but I am talking about the `s == "hello"s"` case (not `const char*`). I have a `std::string` on the left hand side, but then also a `std::string` (constructed from the string literal) on the right side that I thought must be constructed before comparison. – andreee Jun 03 '19 at 14:03
  • @andreee Yes, but that's not the case we're talking about. We're talking about why the string literal version is better _because_ it isn't that. It's possible we're using the term "string literal" differently. We commonly understand this to mean the C-string literal version. – Lightness Races in Orbit Jun 03 '19 at 14:10
  • 1
    Okay I see. _"The comparison to string literal is the "free" version you're looking for."_: By string literal you seem to be talking about plain old C literals, while I was thinking of `std::string_literals`. This might be the problem :-) – andreee Jun 03 '19 at 14:12
  • @andreee: Yup, that's a terminology problem: `"hello"` *is* called a "string literal" in both C and C++ terminology. If you mean a `std::string` literal, you need to say "std::string literal", or possibly "`string` literal" with code just formatting, but the latter is still potentially confusing. – Peter Cordes Jun 04 '19 at 19:23
9

From my understanding, this means that we will need to construct a std::string anyway in order to perform the comparison, so I suspect the overhead will be the same in the end (although hidden by the call to operator==).

This is where that reasoning goes wrong. std::compare does not need to allocate its operand as a C-style null-terminated string to function. According to one of the overloads:

int compare( const CharT* s ) const; // (4)

4) Compares this string to the null-terminated character sequence beginning at the character pointed to by s with length Traits::length(s).

Although whether to allocate or not is an implementation detail, it does not seem reasonable that a sequence comparison would do so.

SE_net4 the downvoter
  • 21,043
  • 11
  • 69
  • 107