Would C++ benefit from immutable strings? Probably not much.
An immutable string is not the same as a read-only string. Immutability guarantees that no change to the observable state of the string may occur outside of what your own code can affect, to the point that if you take any code passing std::string by value, you could just replace it with such an immutable string pointer and everything will work (you cannot distinguish passing such a string by value from passing by reference).
C++ guarantees this only when you create a const object right from the beginning ‒ adding const to an existing object does not make it immutable, and you can remove it via const_cast any time without any issues. This basically means that you cannot make a type immutable in C++, only an object:
const std::string str("hello");
// or
const std::string &str = *new const std::string("hello");
There are actually two guarantees at play here ‒ you cannot modify str via const_cast<std::string&>(str) (undefined behaviour when modifying const object), and you cannot modify the character data via const_cast<char*>(str.data()) (undefined behaviour for data() const). You could theoretically get const_cast<std::string&>(str).data(), and modifying that might be fine for a trivial std::string implementation, but the standard std::string might be optimized so that it stores the character data (up to a size) in the object itself, thus you still risk modifying the object.
How do you pass an immutable object around? You can't.
void f(const std::string &str);
This is no longer an immutable object, but a read-only object ‒ you cannot modify it through str, but it can still change at any time.
To have an actually immutable string, you need to encode this in the type system somehow. The best you can do is to wrap it in another object where it is immutable, like const std::tuple<const std::string> &str ‒ there is no tuple variance that would permit getting this reference from a mutable std::tuple<std::string>, and so, when created, the string is already a const object.
Lastly, you also need guarantees about the lifetime of such an object, since even an immutable object can be deleted. That is thankfully not so complicated ‒ std::shared_ptr<const std::tuple<const std::string>> gives you all the guarantees ‒ once you observe its value, it should stay constant forever. This is pretty much the basis of what languages with immutable strings give you.
Now, to reason about the answer, how would C++ benefit from using such a string? Are there any places where read-only strings are stored, without giving you the control over the specific type? There are not that many I could come up with:
std::exception ‒ constructing it from an error message needs to copy the string to preserve it, but with an immutable string pointer, it could just store that and return in what(). However, you can create your own exception types that do that.
std::locale ‒ likewise, constructing from a locale name could just store the immutable string inside, without copying. However, locale names are not that frequently used, and are short enough to make small string optimization kick in for most of cases.
std::messages ‒ get() could be a primary target for immutable strings, as any other "string catalogs" ‒ retrieval could happen relatively often, and the strings are moderately long and constant enough. Nothing stops you from adding a caching layer to this object however.
I encourage you to find more such examples. Indeed, there are many more cases where strings are accepted and processed, or generated and returned, than simply stored.
That being said, the situation in user code is drastically different ‒ once you store user records or configurations, you become more in need of such a type. So while the C++ language itself might not benefit from it, your code definitely would! Such a type needs to:
- Be easily constructible from a string literal without copying, creating an immutable string with permanent lifetime.
- Be move-constructible from
std::string, taking ownership of its character data.
- Be constructible without copying from other objects with lifetime management that exhibit immutability, such as
std::shared_ptr<std::array<const char, N>> or the aforementioned tuple.
- Be constructible by value from
std::string, char iterators and the usual stuff, copying the data to its internal memory.
- Be "unsafe-constructible" without copying from things like
std::shared_ptr<std::span<char>>, where immutability is not guaranteed by the type. This option needs to be clearly marked as dangerous, and modifying the underlying data in any way during the lifetime of the result should cause undefined behaviour.
- Interface well with existing
std::shared_ptr-based code, preferably be handled as std::shared_ptr itself.
- Allow allocation-less slicing, producing views into its data with the same lifetime.
- Include compatibility with C strings in the form of
const char *data() const for retrieval. Note that sliced strings may be missing the '\0' character at the end, and so there needs to be a way to detect this having happened and return std::shared_ptr<const char*> instead (either aliased to the original data, or to a copy thereof with '\0' at the end).
This interplay between std::shared_ptr and std::string makes such a type definitely preferable to be defined by the standard rather than user code, as some situations could be handled much better without unnecessary allocations or indirection, and the immutability guarantee enables important optimizations.