How to create a constexpr std::vector<std::string> or something similar?

Viewed 1812

So I've been looking around and trying different things but I can't wrap my head around how I would go about creating some collection of strings with constexpr.

What I'm trying to do is basically the following, which obviously doesn't compile:

constexpr std::vector<std::string> fizzbuzz(){
    size_t N = 100;

    std::vector<std::string> result;
    result.reserve(N);

    for (int i = 0; i < N; i++){
        int k = i+1;

        if(k % 5 == 0 && k % 3 == 0){
            result.push_back("FizzBuzz");
        }
        else if(k % 5 == 0){
            result.push_back("Buzz");
        }
        else if(k % 3 == 0){
            result.push_back("Fizz");
        }
        else{
            result.push_back(std::to_string(k));
        }

    }
    return result;
}

I would already be happy if I understood how to do something as simple as:

constexpr std::string fizzbuzz(int k){
    if(k % 3 == 0) return "Fizz";
    else return std::to_string(k);
}

From there I reckon it's only a small step to the complete solution. It doesn't have to be std::strings it doesn't have to be std::vectors.

Oh, and the lower the C++Standard the better.

Edited to help understanding the problem better.

4 Answers

std::vector/std::string doesn't have constexpr constructor before C++20... and even in C++20, the constexpr allocation should not escape from constexpr evaluation, so cannot be used in runtime (for printing).

I don't see a standard constexpr way to transform an integer to a char sequence representation. std::to_string, std::to_chars, std::format are not constexpr.

So instead of homogeneous container, you might use std::tuple, something like (C++17):

template <std::size_t I>
constexpr auto fizzbuzz_elem()
{
    if constexpr (I % 5 == 0 && I % 3 == 0) {
        return "FizzBuzz";
    } else if constexpr (I % 5 == 0) {
        return "Buzz";
    } else if constexpr (I % 3 == 0){
        return "Fizz";
    } else {
        return I;
    }
}

template <std::size_t...Is>
constexpr auto fizzbuzz_impl(std::index_sequence<Is...>){
    return std::make_tuple(fizzbuzz_elem<1 + Is>()...);
}

template <std::size_t N>
constexpr auto fizzbuzz(){
    return fizzbuzz_impl(std::make_index_sequence<N>());
}

int main() {
    constexpr auto res = fizzbuzz<42>();
    std::apply([](auto... e){ ((std::cout << e << std::endl), ...); }, res);
}

Demo

One way to do this kind of stuff is to use the frozen library, which works in C++14, and parts of it work in C++11. (We used it in production in some C++11 code.)

There are a couple things the library offers to make the constexpr happen:

While std::string can make calls ultimately to the dynamic memory allocator, which is not constexpr friendly (unless they made big advances in recent standards that I missed?) frozen::string is basically a string-span pointing to a string constant. So if your data structure is being constexpr initialized, frozen::string never makes an allocation, and that is why it can be constexpr friendly.

The frozen containers are meant to have API very similar to the C++ stdlib containers, but they are not modifiable after construction. Additionally, they are very efficient at run-time -- the maps are based on creating a perfect hash table, at compile time, and they don't make any dynamic memory allocations either.

Here's an example:

#include <frozen/unordered_map.h>
#include <frozen/string.h>

constexpr frozen::unordered_map<frozen::string, int, 2> olaf = {
    {"19", 19},
    {"31", 31},
};
constexpr auto val = olaf.at("19");

This can be very useful if you have a bunch of string constants that need to map to configuration values for your software, or vice-versa.

const variables at file-scope which are constexpr initialized do not have traditional static initialization since C++11. This means, their constructors are not called before main is entered, there is no "static initialization order fiasco". Instead they end up in the BSS read-only memory segment of the executable with the right values already in place. If you have many such maps or they are large, this can measurably improve the startup time of your application, due to not calling malloc and copying a lot of strings around before main is entered.

https://github.com/serge-sans-paille/frozen

std::vector and std::to_string() are not constexpr. Your second function example will work without those and while using std::string_view, like:

constexpr std::string_view fizzbuzz(const int k){ if(k % 3 == 0) return "Fizz"; else return "Buzz"; }

I believe std::string_view is c++17

So with the help of @Jarod42 I solved it at last:

See comments for why this is striked through.

template <int N>
constexpr std::array<char[9], N> solution8(){
    std::array<char[9], N> result{};

    for(int i = 0; i < N; i++){
        int k = i + 1;
        if ((k % 3 == 0) && (k % 5 == 0)){
            sprintf(result[i], "FizzBuzz\0");
        }
        else if (k % 3 == 0){
            sprintf(result[i], "Fizz\0");
        }
        else if (k % 5 == 0){
            sprintf(result[i], "Buzz\0");
        }
        else {
            sprintf(result[i], "%d\0", i+1);
        }
    }

    return result;
}
Related