Search code examples
c++stl

C++ constexpr std::array of string literals


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:

  1. 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.
  2. 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" };

Solution

  • 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 NULs. 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 strings 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 NULs, 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:

    1. 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
    2. For older APIs that accept const std::string&, it constructs a temporary std::string when you call it, but it can use the constructor that extracts the length from the std::string_view so it doesn't need to prewalk a C-style string with strlen to figure out how much to allocate.
    3. For any API that needs a std::string (because it will modify/store its own copy), they're receiving string 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_views than using std::string is case #2 (where if the std::array contained std::strings, 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 strings in exchange for avoiding copies when passing to old-style APIs taking const std::string&.