最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

c++ - Is there a standard way to keep the result of a constexpr std::string function? - Stack Overflow

programmeradmin2浏览0评论

C++ allows constexpr functions that return std::string. For example, using magic_enum to get a space-separated string containing all the enum values.

template <E e>
static constexpr std::string GetAllEnumNamesAsString()
{
    const std::string space = " ";
    constexpr std::array names = magic_enum::enum_names<E>();
    std::string all{ names[0] };
    for (int i = 1; i < names.size(); i++)
        all += (space + std::string{ names[i] });

    return all;
}

The problem is, while a string can be used in a constexpr function, you cannot declare a constexpr string because it uses the heap. So code using this returned string can only declare it as const. I thought that the compiler would at least use the constexpr function to calculate the final string buffer and put that in static storage, but both GCC and MSVC just delegate the entire call to runtime, causing a ton of extraneous code to be generated. Godbolt link:

int main()
{
    // must be allowed at compile time because it's part of a static_assert
    static_assert(GetAllNames<MyEnum>().length() == 17);

    // but compiler makes this pure runtime - not even the final string is placed in static storage to copy to the heap
    const std::string names = GetAllNames<MyEnum>();
    printf("%s\n", names.c_str());

    return 0;
}

What's the best way to get around this to actually use the compiler-calculated string buffer? In my limited understanding, constexpr allows std::string as 'transient' so we need to copy it out of the string to somewhere before the function that uses the string is destroyed (to an std::array?). The trick here is that you cannot use a variable as a template parameter. To get around this, we just need to... call the function twice? The following works, but I must be missing something - is there something standard out there already for this? Or at least a better way to not 'throw away' all the work the compiler can do in constexpr functions returning a string?

template <size_t Length>
class FakeStringView
{
public:
    constexpr FakeStringView(const std::string_view& s1) : _array{}
    {
        for (auto i = 0; i < s1.length(); ++i)
            _array[i] = s1[i];
    }

    constexpr std::string_view Get() const
    {
        return { _array.data(), _array.size() };
    }

private:
    std::array<char, Length> _array;
};

template <typename E>
constexpr auto GetAllNames()
{
    // call it twice - once for length and once for the string
    return FakeStringView<GetAllNamesImpl<E>().length()>(GetAllNamesImpl<E>());
}

constexpr auto fakestringview = GetAllNames<MyEnum>();
std::cout << fakestringivew.Get();

Full godbolt example.

C++ allows constexpr functions that return std::string. For example, using magic_enum to get a space-separated string containing all the enum values.

template <E e>
static constexpr std::string GetAllEnumNamesAsString()
{
    const std::string space = " ";
    constexpr std::array names = magic_enum::enum_names<E>();
    std::string all{ names[0] };
    for (int i = 1; i < names.size(); i++)
        all += (space + std::string{ names[i] });

    return all;
}

The problem is, while a string can be used in a constexpr function, you cannot declare a constexpr string because it uses the heap. So code using this returned string can only declare it as const. I thought that the compiler would at least use the constexpr function to calculate the final string buffer and put that in static storage, but both GCC and MSVC just delegate the entire call to runtime, causing a ton of extraneous code to be generated. Godbolt link:

int main()
{
    // must be allowed at compile time because it's part of a static_assert
    static_assert(GetAllNames<MyEnum>().length() == 17);

    // but compiler makes this pure runtime - not even the final string is placed in static storage to copy to the heap
    const std::string names = GetAllNames<MyEnum>();
    printf("%s\n", names.c_str());

    return 0;
}

What's the best way to get around this to actually use the compiler-calculated string buffer? In my limited understanding, constexpr allows std::string as 'transient' so we need to copy it out of the string to somewhere before the function that uses the string is destroyed (to an std::array?). The trick here is that you cannot use a variable as a template parameter. To get around this, we just need to... call the function twice? The following works, but I must be missing something - is there something standard out there already for this? Or at least a better way to not 'throw away' all the work the compiler can do in constexpr functions returning a string?

template <size_t Length>
class FakeStringView
{
public:
    constexpr FakeStringView(const std::string_view& s1) : _array{}
    {
        for (auto i = 0; i < s1.length(); ++i)
            _array[i] = s1[i];
    }

    constexpr std::string_view Get() const
    {
        return { _array.data(), _array.size() };
    }

private:
    std::array<char, Length> _array;
};

template <typename E>
constexpr auto GetAllNames()
{
    // call it twice - once for length and once for the string
    return FakeStringView<GetAllNamesImpl<E>().length()>(GetAllNamesImpl<E>());
}

constexpr auto fakestringview = GetAllNames<MyEnum>();
std::cout << fakestringivew.Get();

Full godbolt example.

Share Improve this question edited Mar 31 at 12:05 Jarod42 219k15 gold badges196 silver badges330 bronze badges asked Mar 31 at 10:24 user450775user450775 5111 gold badge5 silver badges15 bronze badges 4
  • 1 "call the function twice?" i think we don't have alternative. At least now, we can reuse the same function. Before that std::string/std::vector could be used in constexpr function, we had to create a function to get the size, and then a function to construct the array. – Jarod42 Commented Mar 31 at 11:17
  • 1 for a c++20 way to get compile time allocations to survive to see this Understanding The constexpr 2-Step - Jason Turner - C++ on Sea 2024, in short, you pass the function as a NTTP to a function that constructs and returns this array, and it does it in 2 steps to avoid calling the function twice – Ahmed AEK Commented Mar 31 at 11:55
  • 1 @user450775: [OT] godbold allows to #include url file instead of copy pasted file content (header requires to be self content though). links updated. – Jarod42 Commented Mar 31 at 12:07
  • Ah thanks, will definitely check out that talk! Stuff like that is hard to find searching around for, as the terms have to be created, and thus you don't know what to search for to find it. @Jarod42 - thanks for the godbolt update. I tried including it first but hit errors. – user450775 Commented Mar 31 at 18:04
Add a comment  | 

1 Answer 1

Reset to default 4

In C++20 and even C++23, not really. The best you can do with a function like this:

constexpr std::string my_data() {
  return "some sufficiently long contents here to avoid small strings";
}

is what Jason Turner has called the Constexpr Two-Step (great talk, would recommend). Which is something like this:

template <size_t N>
struct Storage {
    std::array<char, N> contents = {};
    size_t length = 0;

    template <class R>
    constexpr Storage(R&& r)
        : length(std::ranges::size(r))
    {
        std::ranges::copy(r, contents.data());
    }

    constexpr auto begin() const -> char const* { return contents.data(); }
    constexpr auto end() const -> char const* { return contents.data() + length; }
};


template <Storage V>
consteval auto promoted_value() {
    return std::string_view(V);
}

template <auto F>
constexpr std::string_view promote_to_static() {
    constexpr auto oversized_storage = Storage<255>(F());
    constexpr auto correctly_sized_storage = Storage<oversized_storage.length>(oversized_storage);
    return promoted_value<correctly_sized_storage>();
}

constexpr std::string_view s = promote_to_static<[]{ return my_data(); }>();

Note the magic 255 in there. You just have to pick a number large enough. At least this is all at compile-time, so if you pick a number that's too small, your code won't compile. The error might not be the easiest to understand, but a compile error with a cryptic message is still a lot better than an undefined-behavior-driven runtime failure with cryptic symptoms.

The result here is that we're only using exactly the storage that we need. Hence, "two-step" — we're creating two Storages.

And also while string_view isn't usable as a non-type template parameter, s.data() there is a usable non-type template argument — and this solution actually ensures that repeated calls with the same contents produce the same pointer. Which is very neat (and useful!)


In C++26, we're still not going to have non-transient constexpr allocation. But we're at least going to have a much better solution for this, called std::define_static_string:

constexpr std::string_view s = std::define_static_string(my_data());

std::define_static_string basically does the same thing that Jason's implementation does, except first that Reflection allows for a more efficient implementation to begin with — and without the magic 255 — and second that we expect the compiler to implement it more efficiently than even with Reflection.

发布评论

评论列表(0)

  1. 暂无评论