6

Say, I have a constexpr variable that contains all primes less than 216.

constexpr auto primes = [] {
    constexpr int N = 1 << 16;
    std::array<int, 6542> ret;
    bool not_prime[N] = {};
    int prime_cnt = 0;
    for (int i = 2; i < N; i++) {
        if (!not_prime[i]) {
            ret[prime_cnt++] = i;
            for (int j = 2 * i; j < N; j += i) not_prime[j] = true;
        }
    }
    return ret;
}();

Here I manually specified the ret's array size. When I want to change N, I have to re-run this loop to see what the final prime_cnt value is and manually specify it. Particularly, if I increase N, I have to first give an upper-bound guess to avoid segfault.

In C++20, we can dynamically allocate memory in compile-time, and thus we have constexpr vector now. So we no longer need to worry about the upper-bound problem. But such a value cannot be used in run-time, which means, this code is invalid.

constexpr auto primes = [] {
    constexpr int N = 1 << 16;
    std::vector<int> ret;
    bool not_prime[N] = {};
    for (int i = 2; i < N; i++) {
        if (!not_prime[i]) {
            ret.push_back(i);
            for (int j = 2 * i; j < N; j += i) not_prime[j] = true;
        }
    }
    return ret;  // invalid here
}();

So here comes the problem, how do I store such a vector?

An obvious solution is to split this process into two phases. The first one calculates the array size and the second one does the normal calculation. But this needs extra code and computing resources. Is there an elegant and automatic way to achieve this goal?

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
QuarticCat
  • 1,314
  • 6
  • 20
  • 1
    There's no such thing as compile-time dynamically allocated memory. But memory is memory, you can just tell compiler to increase the stack size, and make an array on stack `int[N]`, but it will permanently occupy this memory, without ability to free it during run-time – Alexey S. Larionov May 28 '22 at 11:37
  • Checking cppreference, `std::vector::size` is also `constexpr`. So, I would think it would be possible to use it to declare a `std::array`, and then move the vector's contents into it. – Sam Varshavchik May 28 '22 at 11:38
  • "Did you read the conditions"? Like https://stackoverflow.com/questions/62404438/how-does-constexpr-new-allocate-memory – KamilCuk May 28 '22 at 11:41
  • 2
    @AlexeyLarionov We have it in C++20. Somebody call it transient allocation. – QuarticCat May 28 '22 at 11:45
  • 2
    @KamilCuk I understand the current limitations of compile-time dynamic allocations. There are ways to bypass it (to make it persist), for example, copying it into and fixed-sized array, as I proposed at the end of the problem. The problem is this way is too much painful. – QuarticCat May 28 '22 at 11:49
  • I think that (as of now) the philosophy is "what happens at compile-time stays at compile-time", so an invalicable barrier separates the two world. I wonder if there'll be surprises here – MatG May 28 '22 at 11:50
  • @KamilCuk Check this painful and stupid example https://godbolt.org/z/zT4sqG7hf – QuarticCat May 28 '22 at 11:53

2 Answers2

10

Create a lambda that makes a vector, and use another lambda to create an array

#include <array>
#include <vector>
#include <algorithm>

constexpr auto primes_num_vector = [] {
  constexpr int N = 1 << 16;
  std::vector<int> ret;
  bool not_prime[N] = {};
  for (int i = 2; i < N; i++) {
    if (!not_prime[i]) {
      ret.push_back(i);
      for (int j = 2 * i; j < N; j += i) not_prime[j] = true;
    }
  }
  return ret;
};

constexpr auto primes = [] {
  std::array<int, primes_num_vector().size()> ret;
  std::ranges::copy(primes_num_vector(), ret.begin());
  return ret;
}();
康桓瑋
  • 33,481
  • 5
  • 40
  • 90
1

A std::vector uses dynamic allocations, and in a constexpr any dynamic allocation must be matched by delete. So you cannot constexpr a std::vector like that.

But nothing stops you from using std::array. The number of primes below N can be counted in a constexpr, and then you can use that to give a compile-time size for the std::array:

#include <vector>
#include <array>

constexpr std::size_t num_primes(std::size_t N) {
    std::size_t num = N - 2;
    for (std::size_t i = 2; i < N; ++i) {
        for (std::size_t j = 2; j * j <= i; ++j) {
            if (i % j == 0) {
                --num;
                break;
            } 
        }
    }
    return num;
}

constexpr auto primes = [] {
    constexpr int N = 1 << 16;
    std::array<std::size_t, num_primes(N)> ret;
    std::size_t pos = 0;
    bool not_prime[N] = {};
    for (int i = 2; i < N; i++) {
        if (!not_prime[i]) {
            ret[pos++] = i;
            for (int j = 2 * i; j < N; j += i) not_prime[j] = true;
        }
    }
    return ret;  // invalid here no more
}();

#include <iostream>
int main() {
    for (const auto & x : primes) {
        std::cout << " " << x;
    }
    std::cout << std::endl;
}

The code is pretty stupid as it computes all primes twice, once with the horrible inefficient division test, and once with a sieve. Optimizing this is left to the reader.

Tip: merge the two functions, count primes while you create the sieve. Then create the array and fill it by iterating over the sieve. Or, create a vector during sieve creation, and then copy the vector into the array. As long as the vector is destroyed in the constexpr, the whole is a constexpr.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
Goswin von Brederlow
  • 11,875
  • 2
  • 24
  • 42