Home > other >  Builder patterns with vectors evaluated at compile time (with `consteval`)
Builder patterns with vectors evaluated at compile time (with `consteval`)

Time:06-04

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.

CodePudding user response:

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.

CodePudding user response:

<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.

  • Related