68

There is an implicit conversion from std::string to std::string_view and it's not considered unsafe, even though this surely may cause a lot of dangling references if programmer is not careful.

On the other hand, there's no implicit conversion from std::string_view to std::string using same argument but in the completely opposite fashion: because programmer may be not careful.

It's lovely that C++ has a replacement for a raw const char* pointer, while making it super confusing and stripped to the bone:

  • Implicit const char* -> std::string: OK
  • Implicit std::string_view -> std::string: NOPE
  • Assignment std::string = const char* : OK
  • Assignment std::string = std::string_view: OK
  • Appending std::string += const char* : OK
  • Appending std::string += std::string_view: OK
  • Concatenation const char* + std::string: OK
  • Concatenation std::string_view + std::string: NOPE
  • Concatenation std::string + const char*: OK
  • Concatenation std::string + std::string_view: NOPE

Am I missing something or is this a total nonsense?

In the end, how useful is this string view without all the crucial pieces that make it similar to const char*? What's the point of integrating it into the ecosystem of stdlib while not making the last step to make it complete? After all, if we need an object that represents a piece of a string we could write our own. Actually, a lot of libraries already have done that, years ago. The whole point of making something standard is to make it useful for widest range of use cases, isn't it?

Are they going to fix this in C++23?

GreenScape
  • 5,726
  • 1
  • 26
  • 51
  • 20
    I'm voting to close this question as off-topic because this isn't a question, this is a rant. – Barry Nov 28 '17 at 12:57
  • 2
    This probably belongs on https://www.reddit.com/r/cpp (some C++ committee members read that.) – Nikos C. Nov 30 '17 at 08:29
  • 29
    It seems some unfair that this question, whose discussion would help many programmers, has been closed – lrleon Jun 17 '18 at 15:27
  • 11
    @Barry, this is indeed a question. It's also a rant, but that doesn't make it not a question. The OP is expressing legitimate shock, quite understandable IMO, which doesn't at all detract from the technical legitimacy and usefulness of the question. It would be great if the question could be reopened. – Don Hatch Jul 18 '19 at 19:11
  • _"Seriously, whats't up."_ Sounds like a question to me. ;) – Innocent Bystander Dec 20 '19 at 22:33

2 Answers2

31

The problem is that std::string_view -> std::string makes a copy of the underlying memory, complete with heap allocation, whereas the implicit std::string -> std::string_view does not. If you've bothered to use a std::string_view in the first place then you obviously care about copies, so you don't want one to happen implicitly.

Consider this example:

void foo1(const std::string& x)
{
    foo2(x);
}
void foo2(std::string_view x)
{
    foo3(x);
}
void foo3(const std::string& x)
{
    // Use x...
}

The function foo2 could've used a const std::string& parameter, but used a std::string_view so that it is more efficient if you pass in a string that isn't a std::string; no surprises there. But it's less efficient than if you'd just given it a const std::string& parameter!

  • When foo2 is called with a std::string argument (e.g. by foo1): When foo2 calls foo3, it creates a copy of the string. If it had a const std::string& argument, it could've used the object it already had.
  • When foo2 is called with a const char* argument: A std::string copy has to be made sooner or later; with a const std::string& parameter it gets made earlier, but overall there's exactly one copy either way.

Now imagine foo2 calls multiple functions like foo3, or calls foo3 in a loop; it's making exactly the same std::string object over and over. You'd want the compiler to notify you about this.

Arthur Tacca
  • 6,565
  • 1
  • 27
  • 42
  • 4
    From my experience, most often `std::string_view` is used to optimize parsing of a string. The result of parsing, obviously, cannot be stored in a view, so it should be transferred to a permanent storage (`std::string`). Why should it be so verbose? After all, I am in control and i **should** know where view *may* be converted. – GreenScape Nov 28 '17 at 07:44
  • What you describe is a bad design. A bad design of other piece of code **should not** be a reason to make design of `std::string_view` also bad. Also, programmer, seeing `foo1()` and `foo3()` **should** know about conversions. And if we ignore all this, how does explicit helps in your example? Because of bad design, programmer still forced to create `std::string`, doesn't he? – GreenScape Nov 28 '17 at 07:48
  • 2
    @GreenScape I don't understand the point in your first comment; maybe you could add some example code to your question? I also don't really understand your second point; what exactly is bad design? The fact that `foo3` takes a `const std::string&`? That was extremely common before `std::string_view` existed, and there is a lot of that code lying around. Or the implementation of `foo2`? Because I agree (although it's bad implementation not bad design) but it can easily happen by accident, and you would want the compiler to catch it. – Arthur Tacca Nov 28 '17 at 09:28
  • Just to address the last part of your second comment (sorry I missed it initially): If you get the compiler error in a function that would need a `std::string` multiple times, you could decide to construct it once at the beginning of the function and reuse it for the multiple function calls. Or you could change the function signature to take a `const std::string&`. – Arthur Tacca Nov 28 '17 at 09:36
  • 1
    @ArthurTacca what would you say if `string_view` was replaced by `const char*` in your example ? it's been decades we deal with const char* -> string implicit conversion. `string_view` helps to manipulate temp `const char*` memory, it is obvious that at a moment the `string_view` will trigger allocation by converting into a string, and it is the responsability of the user to ensure where. More, an old code built with `const std::string&` everywhere couldn't translate easily with string_view because of that missing implicit conversion. – Juicebox Apr 08 '20 at 15:28
  • 1
    @Juicebox That is a totally different scenario. For a start, string literals are `const char*` so there needs to be an implicit conversion so that `f("foo")` works when `f` takes a `std::string` (there would be riots in the street if it didn't). Second, you only use `string_view` if you are specifically trying to avoid memory allocation - that's the whole point of my answer - but that is certainly not the whole point of `const char*`. – Arthur Tacca May 06 '20 at 10:35
  • @Juicebox "it is obvious that at a moment the string_view will trigger allocation by converting into a string" Yes it is obvious that a conversion to `string` will trigger an allocation, but there is an implicit conversion then it is not obvious when that conversion is happening. It is very much the responsibility of the compiler, not the user, to catch mistakes that it can. And is it really a problem to write `f(string(x))` rather than `f(x)` if you want to do a conversion? – Arthur Tacca May 06 '20 at 10:51
  • This would be much less annoying if they provide some easy way to do the conversion explicitly. – NO_NAME May 08 '20 at 07:23
  • @NO_NAME As my last comment said, if `f` takes a `std::string` and `x` is a `std::string_view` then you can just write `f(std::string(x))`. I can't imagine anything explicit that could be simpler than that. What were you thinking of? – Arthur Tacca May 08 '20 at 09:23
  • @ArthurTacca Sorry, I didn't realize that the generic constructor works for `std::string_view`. – NO_NAME May 08 '20 at 10:59
  • You say passing `std::string_view` is less efficient than using `const std::string &`. [This test dramatically shows the opposite.](https://www.codeproject.com/Articles/5262072/Cplusplus-17-New-Features-and-Trick#std::string_view) What conditions would make your statement true? – psimpson May 09 '20 at 17:27
  • @psimpson Lots wrong in your comment (a) I said `string_view` is *more* efficient; (b) the answer to "what conditions...?" is in the same sentence from my answer: when "you pass in a string that isn't a `std::string`" (because it avoids a copy); (c) in that microbenchmark you actually do have a `std::string` so there should be no difference; the benchmark is clearly bogus because outputing text to a console, even just copying to a buffer to output later, is hugely more expensive than the different between those parameter types when you start with a `std::string`. – Arthur Tacca May 10 '20 at 11:59
  • It was probably a misstatement, but your post says, after the code block "But it's less efficient than if you'd just given it a const std::string& parameter!". Perhaps that line should be removed, or edited if the "it's" was intended to refer to, maybe, `std::string`. And, yeah, I don't do that sort of benchmarking, generally, so your answer made me suspect they might have chosen a poor test. In reality, you are both trying to say the same thing. I appreciate your response. – psimpson May 10 '20 at 13:25
  • @psimpson Ah sorry, yes I do also say `string_view` is less efficient than `const std::string&`, but only in the particular situation where the `string_view` is in the middle of the `const std::string&` sandwich shown in the code snippet. That is clear if you read that sentence in context with the sentence or two beforehand. What's more, that situation can't happen in practice without an explicit cast (or compiler error!) because there is no implicit cast – remember, that's what the original question was about, and that example is meant to show why the lack of implicit cast is a good thing. – Arthur Tacca May 10 '20 at 15:24
  • I DID read it out of context, probably because I was asking a different question. The inefficiency lies in foo2::foo3, not in foo1::foo2, which is really what I need to think about as I apply it right now. Thank you! – psimpson May 10 '20 at 16:09
  • Out of these five assignments, three work and two don't. Very hard to understand for the beginner, why this is the case (`v` is always a `std::string_view`): `std::string s1(v);` (ok), `std::string s2{v};` (ok), `std::string s3 = v;` (fails), `std::string s4; s4 = v;` (ok), `struct A { std::string a; }; A a { v };` (fails). The first and third case look the same, so why does one work and the other not? Case number 3 and 4 also look similiar, but only one works. And the same is true for case 2 and 5. I understand the rant of the thread opener very well. – Kai Petzke Nov 02 '20 at 16:51
  • @KaiPetzke I definitely agree those examples are problematic. They're especially troublesome because they relate to initialisation, which is infamously a mess in C++. I never claimed that every aspect of `string_view`'s interface is perfect, only that the problem I described is bad enough that fixing it by making the constructor explicit outweighs the convenience of leaving it implicit. Making every aspect of `string_view` clean is simply not possible in C++. Maybe an analogous class could be implemented cleanly in Rust, especially thanks to its lifetime management. – Arthur Tacca Nov 02 '20 at 22:40
6

Because expensive implicit conversions are undesirable...

You only listed one implicit conversion out of your entire set of examples: const char* -> std::string; and you're asking for another one. In all of the other 'OK'-marked functions - allocations of memory are obvious/explicit:

  • Assignments: When you assign anything to an owning object with variable storage size, it is understood that an allocation may be necessary. (Unless it's a move-assignment, but never mind that.)
  • Append I: when you append to an owning object with variable storage size, it is even more obvious it will need to allocate to accommodate the additional data. (It might have enough reserved space, but nobody guarantees this about strings.)
  • Concatenations: In all three cases, memory is allocated for the result only, not for any intermediate object. Allocation for the result is obviously necessary, since the operands of the concatenation remain intact (and can't be assumed to hold the result anyway).

Implicit conversions in general have benefits and detriments. C++ is actually mostly stingy with these, and inherits most implicit conversions from C. But - const char* to std::string is an exception. As @ArthurTacca notes, it allocates memory. Now, the C++ core guidelines say:

C.164: Avoid implicit conversion operators

Reason:
Implicit conversions can be essential (e.g., double to int) but often cause surprises (e.g., String to C-style string).

and this is doubly the case when the unintended conversion performs expensive operations like allocation, which have side effects, and may make system calls.


PS - std::string_view has quite a few gotchas; see this CppCon 2018 talk by Victor Ciura:

Enough string_view to hang ourselves with

So remember it's not some kind of universal panacea; it's another class that you need to use with care, not carelessness.

... an explicit constructor is sufficient.

There actually does exist an explicit constructor of std::string from std::string_view. Use it - by passing std::string{my_string_view} to a function taking a string.

einpoklum
  • 86,754
  • 39
  • 223
  • 453
  • 2
    Why? Is it such a disaster? What is the worst-case scenario? An extra allocation? It can be easily profiled and optimized, if necessary. On the other hand, we have a handicapped API. Handicapped API is not that easy to work-around. Such an API is disgusting to use: tons of boilerplate just to avoid this 'mysterious catastrophic implicit conversion'. And remember, we're talking about a non-ordinary type. That has much more severe gotcha: dangling references. And we're fine with that. But not with an implicit allocation, oh no! – GreenScape Jun 22 '20 at 15:27
  • @GreenScape: We're talking about what happens _by default_, not about what's allowed to happen at all. Also - why sneak in an extra allocation which the programmer didn't ask for? However... this makes me think of something... – einpoklum Jun 22 '20 at 16:50
  • @GreenScape: Why don't you just use the explicit constructor? – einpoklum Jun 22 '20 at 17:32
  • 2
    As I wrote in my original comment: it's a boilerplate. I know nothing about your experience, but according to mine, if you want to use string_view effectively, you're going to use it *everywhere*. Which means that you're going to use explicit `std::string{foo}` *everywhere*. Which is a unnecessary boilerplate and isn't worth trading it just "to avoid accidental implicit conversions". It just isn't worth it! – GreenScape Jun 23 '20 at 05:31
  • 2
    "Which means that you're going to use explicit std::string{foo} everywhere" - if this is the case, then you probably shouldn't be using string_view in the first place. Just use strings then. Most of the point of using string_view's in the first place is to avoid these allocations. – einpoklum Jun 23 '20 at 06:41
  • 1
    That is the point. Desing your API as lightweight as possible (`string_view`) and then let clients decide how to use it. Alas, making decision in favour of `string_view` forces the clients to use boilerplate. Example: you have a const HTTP request object. To avoid allocations you can index it using `string_view` and let clients extract data as: `std::optional find_header(...)`. This allows you to store the whole blob in a single buffer. It's allocation friendly, move-friendly, but not client friendly. – GreenScape Jun 23 '20 at 07:27
  • 1
    @GreenScape, I totally agree with you. I decided to replace "std::string const &" with "string_view" for all possible function arguments in my libs, and that ended up costing me literally days of work boilerplating my code to fix all the sudden implicit conversion errors (which were all desirable conversion in the context of my program). It got me thinking that the comittee is so wrong on this matter, because is not their role to patronize the way I code. – Jango Jul 27 '20 at 20:36