3

I am attempting to create a class that follows the builder pattern and also runs completely at compile time (with the use of the new consteval keyword in C++20), but whatever I try doesn't work. For example, this won't work:

#include <vector>

class MyClass
{
private:
    std::vector<int> data;

public:
    consteval MyClass& setData()
    {
        this->data = {20};
        return *this;
    }

    consteval std::vector<int> build()
    {
        return data;
    }
};


int main()
{
    std::vector<int> data = MyClass().setData().build();
}

Which gives the error "<anonymous> is not a constant expression". This led me to believe I should instead return copies of the class instead:

#include <vector>

class MyClass
{
private:
    std::vector<int> data;

public:
    consteval MyClass setData()
    {
        // https://herbsutter.com/2013/04/05/complex-initialization-for-a-const-variable/
        return [&]{
            MyClass newClass;
            newClass.data = {20};
            return newClass;
        }();
    }

    consteval std::vector<int> build()
    {
        return data;
    }
};


int main()
{
    std::vector<int> data = MyClass().setData().build();
}

Yet, I got the same error. How should I use a constant-time builder pattern in C++? It seems this only occurs with vectors, and I am using a version which supports C++20 constexpr vectors.

Ayush Garg
  • 2,234
  • 2
  • 12
  • 28

2 Answers2

6

Your code does not compile because current C++ only permits "transient" allocation in constant expressions. That means that during constant expression evaluation, it is permitted to dynamically allocate memory (since C++20) but only under the condition that any such allocations are deallocated by the time the constant expression is "over".

In your code, the expression MyClass().setData() must be a constant expression because it is an immediate invocation (which means a call to a consteval function, other than one that occurs inside another consteval function or inside an if consteval block). The expression MyClass().setData().build() also must be a constant expression. This implies that, while MyClass().setData().build() is being evaluated, dynamic allocations are permitted, but there must be no "surviving" allocations at the end of MyClass().setData() nor at the end of MyClass().setData().build().

Since there is no way to prevent the result of MyClass().setData() from having a live allocation, you must only call it inside an enclosing consteval function or if consteval block. For example, the following will be valid:

consteval int foo() {
    return MyClass().setData().build()[0];
}

Notice that the temporary MyClass object (and thus the std::vector<int> subobject) will be destroyed, and all dynamic allocations will therefore be cleaned up, just before foo() returns.

You want to keep the vector around after the outermost consteval function completes? Sorry, you can't do that---not in the current version of C++, at least. You need to copy its content into a std::array or some other kind of object that doesn't use dynamic allocation.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • Then how do `constexpr std::vector`s work? I assumed that a vector could be created at compile time because of this new feature. – Ayush Garg Jun 04 '22 at 01:00
  • @AyushGarg You still cannot have a `constexpr std::vector` variable. But you can have a `std::vector` variable inside a constant expression evaluation (i.e. in a `constexpr` function). That's the new part in C++20. – user17732522 Jun 04 '22 at 01:02
  • @user17732522 Oh, I see... so, the heap just doesn't work. How would you suggest I copy into an `std::array`? The reason I was using `vector`s was because they're much easier to create elements in (as I am adding many more elements into the data during building). – Ayush Garg Jun 04 '22 at 01:06
  • 1
    @AyushGarg Heap does work. It is just separate from the runtime one and must be properly released before the constant evaluation ends. See for example [this recent question](https://stackoverflow.com/questions/72415239/how-do-i-store-the-compile-time-dynamically-allocated-memory-so-that-it-can-be-u) for how to return the vector contents as an array. – user17732522 Jun 04 '22 at 01:10
  • @user17732522 Sorry, that's what I meant about the heap. Thanks so much for the help! Feel free to make an answer covering how to do that, and I'll accept it. – Ayush Garg Jun 04 '22 at 01:12
1
<source>: In function 'int main()':
<source>:24:46: error: '<anonymous>' is not a constant expression
   24 |     std::vector<int> data = MyClass().setData().build();
      |                             ~~~~~~~~~~~~~~~~~^~

Notice how the compiler unlerlines MyClass().setData().

setData() returns a reference to *this, which is of type MyClass and not a const. You actually just modified it in setData(). A reference to a non-const can't be a constant expression.

You can change that by returning by value: consteval MyClass setData()

But then you run into the problem that in a constexpr all calls to new must be balanced with calls to delete and elided. But std::vector calls new and you never destroy the vector. So you get:

/opt/compiler-explorer/gcc-12.1.0/include/c++/12.1.0/bits/allocator.h:182:50: error: 'MyClass::setData()()' is not a constant expression because it refers to a result of 'operator new'

You can do consteval if you use std::array. Or some other container that doesn't use the heap.

Goswin von Brederlow
  • 11,875
  • 2
  • 24
  • 42
  • The issue with using `std::array` is that I can't add or remove elements, making it much harder to use. What do you suggest I use instead/how should I manage adding more elements? (I technically can calculate how many I need, but it can be difficult as there is a bit of math involved in figuring out how many elements to add). – Ayush Garg Jun 04 '22 at 01:08
  • Generate a vector with all the elements, then create an array of the same size, copy the elements and destroy the vector. – Goswin von Brederlow Jun 04 '22 at 02:16
  • I didn't realize that you can use the result of `vector.size()` to use in the type of the array. Thanks! – Ayush Garg Jun 04 '22 at 02:18
  • Only in a constant evaluation context. – Goswin von Brederlow Jun 04 '22 at 02:50