> libstdc++ rejects the following code (Godbolt [1]). (Microsoft accepts, but I think that might be an MSVC bug.)
This is not a bug in MSVC, rather it is due to MSVC implementing std::string SSO differently than gcc or clang. Instead of initializing `data` as pointing to the internal buffer for small strings, MSVC uses the string's capacity to determine whether to access `data` or the internal storage [2].
Hence this code compiles, as it's not initializing the string using a stack address. [3]
I spent a while porting a codebase that was written by someone far more clever then smart. They had discovered that you could clear a string under MSVC by `memset()`ing over it.
Obviously, this exploded in linux. That was a fun bug to track down.
On MSVC, an all-zero string object is the correct representation for a std::string containing "" (and the memory leak is easy to miss if you don't do it too much).
On both Clang and gcc, an all-zero string object is not using the short-string optimization -- it is a string with size zero, capacity zero, and an external buffer of nullptr. (On clang this happens because capacity is even, and on gcc because &str->buf != &str). Any code that attempts to access its null terminator will dereference the null pointer and crash immediately.
I find that there's no use case for a constexpr std::string. Maybe I'm just not imaginative enough. If I want a string literal constant, I use string_view which can be constexpr. If somehow I need an actual std::string object, I use a regular const not constexpr. What would be a use case that prompts the author to explore using constexpr with std::string?
It enables users to process string at compile time. You can implement a constexpr getRFC3339DateString(int year, int month, int day) -> string and then construct a constexpr string list.
The important part is that all the intermediate strings used during the computation are constexpr, to guarantee that the work happens during compilation.
Also, constexpr symbols can be demoted to regular const as needed by the compiler if necessary, such as when getting a pointer to one. constexpr doesn't mean "compile-time only", it means "compile-time compatible"
Right - you can use the std::string at compile time but since it allocates dynamically you need to copy to a fixed size char[]/std::array to use at runtime.
The constructor of std::string is constexpr in C++20. A runtime call to 'strlen' (or equivalent) can be optimized out by computing the string's length at compile time and initializing the object accordingly.
Oh, I don't doubt I'm missing the point. That's why I'm asking. My C++ experience is from the 1990s. Otherwise I do Python and C.
I think of constexpr functions as something which occur at compile time, and I thought this linked-to piece was about what can take place inside those functions.
As I understand it, you want to create a constexpr string so it can be passed to a char*. The issue is this requires heap allocation (for large enough strings, based on the compiler implementation).
The C programmer in me wonders why you don't "char[]" allocate the static memory buffer, then pass around a string view, so the constexpr doesn't need heap at all.
> As I understand it, you want to create a constexpr string so it can be passed to a char*. The issue is this requires heap allocation (for large enough strings, based on the compiler implementation).
There's a few reasons. Avoiding the heap allocation is one, yep. There just shouldn't be a difference between a compile time string and a runtime string. As a programmer I don't want to have to deal with "which flavor of string am I dealing with".
The issue with passing to a c API (I used fopen, but think of any c library - zlib/gzip, openssl, sqlite) is that the string needs to be null terminated, and string view doesn't guarantee null termination.
int foo(std::string_view view) {
return strlen(view.data());
}
std::string str("hello world");
foo(str):
std::string_view view = str;
foo(view);
view.remove_suffix(6); // now view is just "hello" with no null termination
foo(view); // this is wrong.
The problem isn't _specifically_ constexpr std::string in this case, it's that the tools that you use to generalise working with runtime strings, char buffers, and views aren't fit for purpose.
In the 1990s there was only one string type - NUL-terminated characters. ;)
I keep reading about all the neat algorithms (and multi-threading) support in modern C++, but the complexity of the decades of history and the difference in mindset between {C, Python} and C++ have made me shy away.
I think I'll keep mum from a distance in future discussions until I decided to (re) take up the language seriously.
> In the 1990s there was only one string type - NUL-terminated characters.
Indeed. If I could wave a magic wand, I would add a fat pointer to the language that we use to represent strings and arrays, and that we can automatically convert to/from const char* at compile time. This would replace string_view and span. All of the functionality of std::string would be available to it, and a call to strlen, or strcpy on it would work via the length, rather than the null terminator.
Hard-coding a relative file path seems like an obvious example. For example a config file with settings. The file will always have the same name so heap allocations and mutability are not needed.
My config file paths have either been constant (".dotfile") or required run-time information that could not be constexpr'ed. I don't see how constexpr would be useful.
Then again, I am primarily a Python and C programmer, so there is likely some underlying reasoning I'm missing.
in C++ cannot change after construction, but is not compile time constant. This means that it has to run some code and allocate some memory at runtime.
In contrast constexpr is fully compile time and ends up in the read only portion of the binary. No code code execution or allocations necessary.
`constexpr char kFoo[] = "bar"`, then kFoo can be used like an std::string and passed into things that take const std::string& or string_view. The length is part of that. Or is that actually doing a copy?
I think you still need the length though unless you can guarantee the string will never have any nulls in it. In Windows, there are API calls that take strings with embedded nulls (and that are typically terminated with a two nulls in a row).
So you might need to deal with a string that looks like "foo\0bar\0\0".
> I find that there's no use case for a constexpr std::string. Maybe I'm just not imaginative enough.
Putting together strings through string interpolation involving somewhat expensive operations is a common usecase. Given the choice, it's preferable to not have to compute them each and every single time a function is invoked.
You can use `std::array<char>` (which, as it happens, is a literal type) as an intermediary when performing materialization. A nice toy example is to build a comma-separated string listing enumerators:
Note that you have to generate the constexpr std::string twice; once for its size, once for its contents. But you assume that the compiler can memoize the result.
It's really unfortunate the heap allocation can't be in .ro.data for constexpr std::string. It might be an invasive change, but it would beat the hell out of const char* there for the reason of avoiding a copy onto the heap.
This explanation from isocpp is the one that has always guided me:
Read the pointer declarations right-to-left.
* const X* p means “p points to an X that is const”: the X object can’t be changed via p.
* X* const p means “p is a const pointer to an X that is non-const”: you can’t change the pointer p itself, but you can change the X object via p.
* const X* const p means “p is a const pointer to an X that is const”: you can’t change the pointer p itself, nor can you change the X object via p.
And, oh yea, did I mention to read your pointer declarations right-to-left?
A lot of folks write char const * (and T const& in C++), and the "rule of thumb" is to read the declaration right to left. In this case, pointer (to) const char. Works also with multiple consts or levels of indirection.
`char * const a` is a different type from `const char *a`.
In the first, the variable `a` is const, but `*a` is not const. That is, you cannot change the value of `a` but you can change what it points to.
In the second `a` is not const, but `*a` is. That is, you can change `a` but you cannot change the value it points to.
I feel that your confusion is tied up in not understanding how C and C++ type declarations work, as is your insistence on `char* a`.
Dennis Ritchie (the C language designer) intended it to be written as `char *a` and talked about this at length over the years as an important design decision: variable declaration follows _usage_, so it’s `char *a` because the type of `*a` is `char`.
For better or worse, Stroustrup took this design decision into C++.
Sure both will parse in this simple case, but keeping this in mind is also the way that more complex declarations make sense.
It was developed by a C++ expert to fit into the C++ standard, so it has constexpr everywhere. But for this reason, we cannot use memcpy inside its methods and have to wait for a new standard with constexpr memcpy. Note: memcpy is to satisfy strict aliasing - we can use std::bit_cast instead, but there was some trouble.
For many years the best language(s) experts have been trying to have compile time and run time strings live together happily ever. They come tantalizingly close but don't quite succeeded. Maybe it is time to entertain the idea that even if compile and run time strings look so similar as to be almost the same - maybe they are not? Maybe they are actually more dissimilar than they appear? Compile time strings to me look more like symbols table: strings unique and ideally sorted. The offset can be a handle - then uniqueness means we can == or != compare handles rather than strings. If the table is sorted, then < > <= >= compares of symbols via their offsets just works too.
> In real life, there’s basically no reason ever to use constexpr on a stack variable
Wait what? This is what i was doing right now. It defines a local compile time constant and hence is an important mean to express an abstract concept.
You could use a macro, but we are told to get rid of them. And a local const variable is an entirely different thing.
> In real life, there’s basically no reason ever to use constexpr on a stack variable; you’ll use it only on globals or as part of the set phrase static constexpr.
Hmm… this is just unnecessarily dogmatic. I use constexpr all the time, for example, for local numerical constants that don’t need to be a global.
Why not use a static local constexpr? That seems like it would be faster to me due to not needing to allocate stack space and copy the value to the stack. You still get the benefit of not needing a global.
Is accepting 11-char strings but not accepting 19-char strings for constexpr, specified in the C++ specification, or is this just behavior that different compiler vendors are implementing differently?
If the latter, then it is pretty annoying that non-standard behavior is happening now related to standard libraries implementations and constexpr.
My C++ knowledge is from the old days. What book would one recommend to learn
_and understand_ these new fangled concepts like "constexpr", "constinit", std::move, std::unique_ptr, closures, etc.
Not to derail, because frankly I think it's very weird to even bring Rust up here, but `const fn` is a thing in Rust and is guaranteed to execute at compile time.
It is more limited, no question. But you don't have to hope that the compiler will do things, it is guaranteed to do them.
> It’s not any weirder than promoting rust under discussions about c++
Feels like a straw man? I never promoted Rust, certainly I didn't bring it up unprompted. I corrected someone who brought it up.
> unless you think any mention of rust must be positive.
Again, feels like a straw man. I said nothing of the sort.
> There is pretty much nothing useful, this post included, that can be done with const fn in rust.
I feel like you're simultaneously upset that I corrected your post... but also you're challenging my point, in an attempt to pursue discussion? It's confusing because you're wrong but there's also an interesting discussion to be had - although I think this post covers the general issue of "what is a pointer at compile time" quite well.
I didn’t accuse you of doing it, I said you only find other people bringing up rust weird if it’s not positive. Unless you want to agree that it’s always weird to bring up rust in a discussion about c++?
I’m not upset about anything. You said I’m wrong, so show me how you use const fn to make a heap allocated string at compile time as done in this post.
Rereading that's perhaps the issue, the first time I read it I literally could not comprehend what they were saying, I thought it was an ill formed sentence. I see now that it's just worded poorly and also a very odd thing to bring up.
You thought I accused you of promoting rust. I didn’t. I said that someone who complains when rust is criticized in a conversation about c++ should complain equally when rust is promoted, which should not be a controversial statement if you have any consistency.
We are discussing a compile time string. So how can you use const fn to make a compile time string in rust with no macros?
> I said that someone who complains when rust is criticized in a conversation about c++ should complain equally when rust is promoted
Right, this is a straw man. I've never said anything at all about people criticizing C++ and promoting Rust. You're creating an argument out of thin air and attacking it.
> We are discussing a compile time string.
Incorrect. Here is what you said:
> Much more than rust, that’s for sure, where you are forced to stick everything in a macro or pray the compiler is smart for compile time programming.
In no way did you scope it to creating heap allocating strings. Even the post, which ultimately discusses that, discusses other things.
For example, you can write a substr function (firstName) using a const fn in Rust. No macros, no "praying" that the compiler will optimize things.
It would be very odd to limit the discussion to a `string` since this post focuses on how difficult and complex that is in C++, so using it as a "Rust can't even do this" would be sort of ironic.
Anyways, I've engaged in good faith in this discussion for far too long. Your posts are borderline incoherent, frankly, and I think any reader of this thread will have long gotten the message that your initial post was both irrelevant and incorrect.
I said “it’s not any weirder than promoting rust under discussions about c++”. That’s not claiming you said something about promoting rust, that’s asserting that if you have any consistency then you should find that weird as well.
The title of the post asks how constexpr c++ strings are. I answered with “more than rust’s”. The existence of const fn has yet to prove me incorrect.
This is just genuienly awful for the user.
Put yourself back to the times when you first started to learn C++, maybe programming in general. And now you have to wonder why one of your string constants is just fine and the other one gives you some weird error in allocator.h or something...
C++ is a language that was expressive enough to have inside the mechanic that is useful for _implementing_ another language for template metaprogramming, but that another language is built from weird building blocks and they do not look natural at all.
that when I did watch for the first time did remind me A LOT the memes about different levels of haskell engineers writing factorial function, with people somewhere in the middle implementing their own numerics based on basic number theory things. This presentation is literally exact scenario - you take one thing and start building poor man's programming language using it. And it's horrific.
Problem with C++, that it is extremely slowly addressing is that it doesn't have many facilities to improve that nested informal templated language built on top of templating constructs. C++ is getting them, but it at glacier pace. And when I see that presentation after having exposure to other languages, I just feel that this is so much waste here, it could've been much better if language had better thought put into metaprogramming part, instead of letting random crazy geniuses to build hacks on top of hacks to get something useful.
Unfortunately for C++, it gives significant problem for starting. Because to learn something, it is very useful to look into how standard things, like stdlib is implemented. But what you see, often, is not C++, it is that another _thing_ that is using c++ constructs to have its own thing. That has its own patterns and quirks (and there are many of them, so many).
C++ went into local maxima by letting to do many things by various template tricks, but in the end I think that this is dead end. C++ need real metaprogramming, type level programming, you name it - that can be done in (constrained) C++, not in some fungus that has grown on top of template language.
This seems kinda backwards? C++ has gotten as complex as it has mostly because it’s been the pragmatic default for big, performance sensitive applications consistently for like 30 years and the clear up-and-comer for ~10 more.
Java can be really fast and handle big codebases too, and it’s also been huge for long enough it can buy a beer, and it’s also getting a little hairy.
Uh, other languages in the niche seem to be starting their runs at comparable levels of complexity, if they’re as successful as it looks like, writing them will be quantum chromodynamics in another 30.
> Java can be really fast and handle big codebases too, and it’s also been huge for long enough it can buy a beer, and it’s also getting a little hairy.
IMO Java is so simple you can teach it to any programmer in a day or two. I'm not sure which part are "hairy", maybe frameworks?
Memory order is hard but that's only very rarely used in idiomatic Java, and mostly through the atomic wrappers. Synchronized is simpler than equivalent barriers in almost all other languages.
There aren't that many keywords, no operator overloading, barely any metaprogramming (just annotations), almost no compile flags, etc.
I guess tuning the runtime can be hairy, and some of the standard lib stuff is weirdly over complicated (nio vs io), but as far as languages go, Java has been incredibly conservative. In fact, a lot of Java app dev would probably be easier in practice if it had more features.
I think i understand what you say, but i don't think this is in contrary to my statement of C++ having become a language of which features are often reasoned about for the sake of the language itself in stead of for the sake of the goal of the language, namely writing and maintaining computer programs.
Luckily, one can just ignore all the modern C++ shenanigans and use it as not much more than C with classes and some basic templates for generic containers. Those are too useful to give up.
Is everyone on a team of one in C++ land? I kind of want to learn a bit of C++ but to be honest, I’m a bit scared off by the fact that it’s only workable if you stick to a specific style. If I learn C++, will I be completely lost the moment I change teams? Presumably they’ll have their own set of practices they like to follow that I’ll be expected to be productive with.
Most large teams totally ignore whatever the standard library is doing, because it's largely just bad, over engineered and slow, both to compile and at runtime. They then have their own developed from scratch. It would be funny if it wasn't so sad.
For example, even though both Chromium and Unreal Engine are using C++, good luck finding basically anything high level in common. All of the naming schemes are different, the formatting is different, the high level libraries are unrecognizable. It almost feels like these projects are written in different languages.
Beneath all of that, they do make use of the same low level mechanics. A pointer is still a pointer, no matter what libraries you build on it. A stack allocation is still a stack allocation, passing something by a const reference is still the same, passing universal references to container templates is still the same. Includes work the same everywhere. And so on.
Basically, unlike most other languages, you want to have a solid understanding of these underlying mechanics so you can then quickly understand whatever high level libraries the project decided to build on top and hit the ground running using those. There's gonna be some kind of resizable array thing in every project.
But yes, you're basically starting over re-familiarizing yourself with the language on every large project or framework you explore. I've been using C++ for many years and never once used a std::string, since each framework has its own clearly superior string...
You want to be on a team that contains a C++ expert or two, and they should conscientiously and continuously evolve your code base to incorporate more static time correctness checks and to be more in line with where the standard library is heading.
C++ was accidentally highly expressive. Since it was accidental, much of this expressiveness was only available via convoluted and inscrutable metaprogramming. Nonetheless, this expressiveness turned out to be highly useful in practical scenarios, which established the value of that expressiveness.
If you fast-forward to C++20, metaprogramming is mostly, though not always, concise and easy to follow. The language was redesigned to make template hacks that people found highly useful into first-class features of the language. For reasons of backward compatibility, you can still express these things the old ways. But it isn’t required anymore, there are concise and direct ways of expressing most metaprogramming things you might want to do.
The extreme expressiveness of modern C++ in application domains with unusual requirements remains its greatest strength. For pure systems languages, nothing else can express as much in a type-safe way in so few lines of code, almost entirely due to its metaprogramming facilities.
Er, I'm way out of touch with modern c++, what implementation do you buy? Borland used to have tons of little helpers and utils to smooth over some rough edges.
I'd imagine ms vc++ has tons of stuff to support this?
Again, I haven't used a commercial compiler in a while. But that seems like really standard stuff you get with them, yeah?
This is not a bug in MSVC, rather it is due to MSVC implementing std::string SSO differently than gcc or clang. Instead of initializing `data` as pointing to the internal buffer for small strings, MSVC uses the string's capacity to determine whether to access `data` or the internal storage [2].
Hence this code compiles, as it's not initializing the string using a stack address. [3]
[1]: https://godbolt.org/z/1ErrKjdbq
[2]: https://devblogs.microsoft.com/oldnewthing/20230803-00/?p=10...
[3]: https://godbolt.org/z/1MaxdGfvj