I've been happily using the following style of constant string literals in my code for awhile, without really understanding how it works:
constexpr std::array myStrings = { "one", "two", "three" };
This may seem trivial, but I'm hazy on the details of what is going on under the hood. From my understanding, class template argument deduction (CTAD) is used to construct an array of the appropriate size and element type. My questions would be:
- What is the element type of the std::array in this case, or is this implementation specific? Looking at the debugger (I'm using Microsoft C ), the elements are just pointers to non-contiguous locations.
- Is it safe to declare constexpr arrays of string literals in this way?
I could do this instead, but it's not as tidy:
const std::array<std::string, 3> myOtherStrings = { "one", "two", "three" };
CodePudding user response:
Yes, this is CTAD deducing the template arguments for you. (since C 17)
std::array
has a deduction guide which enables CTAD with this form of initializer.
It will deduce the type of myStrings
to
const std::array<const char*, 3>
The const char*
is the result of usual array-to-pointer decay being applied to the elements of the initializer list (which are arrays of const char
s).
const
in front is a consequence of constexpr
.
Each element of the array will point to the corresponding string literal.
constexpr
is safe and you can use the array elements as you would individual string literals via const char*
pointer. In particular trying to modify these literals or the array via const_cast
will have undefined behavior though.
const std::array<std::string, 3>
also works, but will not be usable in constant expressions. constexpr
is not allowed on this because of std:string
.
CTAD can also be used to deduce this type though with the help of string literal operators:
#include<string>
using namespace std::string_literals;
//...
const std::array myOtherStrings = { "one"s, "two"s, "three"s };
or since C 20:
const auto myOtherStrings = std::to_array<std::string>({ "one", "two", "three" });
CodePudding user response:
As user17732522 already noted, the type deduction for your original code produces a const std::array<const char*, 3>
. This works, but it's not a C std::string
, so every use needs to scan for the NUL
terminator, and they can't contain embedded NUL
s. I just wanted to emphasize the suggestion from my comment to use std::string_view
.
Since std::string
inherently relies on run-time memory allocation, you can't use it unless the entirety of the associated code is also constexpr
(so no actual string
s exist at all at run-time, the compiler computes the final result at compile-time), and that's unlikely to help you here if the goal is to avoid unnecessary runtime work for something that is partially known at compile time (especially if the array
gets recreated on each function call; it's not global or static
, so it's done many times, not just initialized once before use).
That said, if you can rely on C 17, you can split the difference with std::string_view
. It's got a very concise literal form (add sv
as a prefix to any string literal), and it's fully constexpr
, so by doing:
// Top of file
#include <string_view>
// Use one of your choice:
using namespace std::literals; // Enables all literals
using namespace std::string_view_literals; // Enables sv suffix only
using namespace std::literals::string_view_literals; // Enables sv suffix only
// Point of use
constexpr std::array myStrings = { "one"sv, "two"sv, "three"sv };
you get something that involves no runtime work, has most of the benefits of std::string
(knows its own length, can contain embedded NUL
s, accepted by most string-oriented APIs), and therefore operates more efficiently than a C-style string for the three common ways a function accepts string data:
- For modern APIs that need to read a string-like thing, they accept
std::string_view
by value and the overhead is just copying the pointer and length to the function - For older APIs that accept
const std::string&
, it constructs a temporarystd::string
when you call it, but it can use the constructor that extracts the length from thestd::string_view
so it doesn't need to prewalk a C-style string withstrlen
to figure out how much to allocate. - For any API that needs a
std::string
(because it will modify/store its own copy), they're receivingstring
by value, and you get the same benefit as in #2 (it must be built, but it's built more efficiently).
The only case where you do worse by using std::string_view
s than using std::string
is case #2 (where if the std::array
contained std::string
s, no copies would occur), and you only lose there if you make several such calls; in that scenario, you'd just bite the bullet and use const std::array myStrings = { "one"s, "two"s, "three"s };
, paying the minor runtime cost to build real string
s in exchange for avoiding copies when passing to old-style APIs taking const std::string&
.