Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
What's so hard about constexpr allocation? (brevzin.github.io)
82 points by signa11 on July 25, 2024 | hide | past | favorite | 35 comments


My current solution to std::vectors not being constexpr is initializing a std::array with an immediately invoked lambda expression. That lets me algorithmically determine the size of the array at compile-time, which is the main use I've wanted out of a constexpr std::vector.


It seems like the C++ language devs are always trying to rein in horses that have already left the barn.


Yes. Here though the ostensible topic is constexpr allocation, the real topic is adding deep const.


It seems like C++ would be simpler if were either just all immutable (like Haskell) or embraced using the full, mutable language for metaprogramming (Jonathan Blow's Jai). Of course that's a horse that left the barn before most of the current barn was even built.


I suppose one could opt-into default-immutable and make it Rust-like. That'd be nice.


I don't understand why constexpr allocated memory can't be freed at runtime. It's not so different from statically allocated memory being freed at runtime. Maybe it requires hooks into malloc so the compile time allocations can be known at the start of the run?


There may be other limitations as well, but I think of it as a question of where to store the object after compilation, but before runtime.

Clearly, it must be saved in the compiled artifact itself. We have an example of this already with string literals. This is part of the compiled artifact, and cannot be changed at runtime. So, in theory we could allow compile time allocations to be accessed at runtime, on the condition that they are never deallocated.

However, this would introduce a large complication. Objects may contain pointers to other objects, where string literals cannot contain pointers to other string literals. We don’t know where the artifact will be loaded into memory, so those pointers may not be valid. To support them, they would need to be updated as the artifact is being loaded.

There is precedent for this, as updating pointers when loading a library is one way to implement shared libraries. However, shared libraries only require this for externally-exposed symbols. Internal function calls can use relative jumps instead. For saving an object, which may contain pointers to itself, private objects would also need to have their pointers updated, which would add to the overhead. So, rather than requiring compilers to add what amounts to an additional dynamic-linking step, the standard forbids the runtime use of a compile time allocation altogether.

(At least, that’s my overall understanding, and I would be happy to be corrected on it if wrong.)


I must be missing something here, because my first thought was "Any constexpr must be const all the way down." Meaning:

- Constexpr objects must be fully const (i.e. they cannot be modified, and non-const methods are not allowed to be called).

- Constexpr objects cannot allow access to mutable data (directly or indirectly). Maybe this is where the devil is in the details? Not sure... It seems that pointers to mutable via constexpr would be a niche thing anyway.

- Constexpr objects always exist for the duration of the program.

- Any allocations made during construction are done via a "const pool" allocator that no-ops deallocation, so that nothing blows up at shutdown. If everything is const/constexpr, the total allocation size should be decidable at compile time, I think?


Does “const” mean “immutable” and “constexpr” mean “compile-time” in C++? That’s funny.


constexpr was originally intended for "things that can be computed at compile time". But for something to be computable at compile-time, it would have to also be immutable.


I can imagine a for loop with `i` which computes something over a static value where `i` isn’t immutable but is needed to compute the “constexpr” value.


But if `i` changes over time, then it can't be computed at compile time, because its value is dependent upon at what time it is accessed.


Looping over the calendar months seems to make sense to me.


> It is arguably pointless to pursue allowing persistent constexpr allocation if the allocation itself cannot be used as a constant-expression.

It's useful to ensure that some things get calculated at compile time even if all eventuall access is at runtime.


How does Rust solve this problem?


It doesn't, you can not dynamically allocate memory in const functions even temporally. Rust was designed with the benefit of hindsight, so the well understood parts of const evaluation are stable and don't paint the syntax in a corner requiring `constexpr` and `consteval` keywords in the future.


Deep const: since you have a (const) ref to the variable, you cannot access any of its parts mutably. Of course, RefCell and co are not allowed, I suppose.


Internally mutable types are allowed, but doing internal mutation isn't.

So a const function can return a cell, for instance, as long as the cell isn't mutated inside the function.


unpopular opinion, I think constexpr and similar are anti patterns. if you want to memoize values, run a separate script to do that. a computation is not and cannot be constant, this is just some sugar so that people can pretend it is.


The compiler is a series of such scripts. With constexpr/consteval/constinit, templates, and reflection, C++ is slowly converging on hygienic macros.

Also "a computation is not and cannot be constant" depends on what is meant precisely by "computation" and "constant." Is "23 * 48" a constant, or is it a computation? What about "2208/2"? What about "1.104e3"?

You can define "constant" as "something that is not computed but that exists in the program as an immediate value." In that case you're right but only by definition. And the compiler must still "interpret" the immediate value. It's a slightly fuzzy distinction in my eyes.


> a computation is not and cannot be constant

Sure it can - but you know that - a compile time computation can be a run time constant. There’s loads of reasons to want constant values built out of compile-time computation. You haven’t had to write any high performance code?

Start with something like

    constexpr float angle_threshold = 45.0f;
    constexpr float degToRad        = M_PI / 180.0f;
    constexpr float x_threshold     = cosf( angle \* degToRad );
The nice thing about having this be an expression is it’s semantic, and I can tune my angle in degrees, not have some raw number that isn’t adjustable.

Maybe I need a small table of these, suddenly running a little code at compile time looks great. Doing it during compilation is a lot nicer than having a script dependency and having to write a build step for it.

The bigger issue with constexpr specifically is that it doesn’t actually guarantee compile time evaluation…


yet any remotely competent compiler would have turned degToRad to a constant regardless.


degToRad wasn’t the point, the list of cosines being computed at compile time was the point… or something similar, or something different… use your imagination. I used cosf() because it’s clearly a function call doing compile time compute, and I suggested a list because it makes a more clear case for compile time compilation, and because that might start to get into the reasons one would want constexpr allocation. And yes of course a good compiler absolutely will turn things into constants if it can, regardless of how they’re declared, including without any special const* specifiers at all. That is not why either const or constexpr exist, right?


That's a rather dickish way of completely missing the point


I apologize for offending, it certainly wasn't the intent.

But I don't believe one can justify the existence of constexpr simply by pointing to code that absolutely any compiler would make into constants absent it.

Perhaps in your example one could say that the value is that you really want the compilation to abort should changes happen to the code that make it impossible for the compiler to turn it into constants, since doing so is performance critical-- I think that would be a fair point.


Sounds like you’re close to justifying the existence of constexpr, that sounds similar to the summary of what it’s for.

I wasn’t trying to justify the existence of constexpr, I was justifying the existence of compile time constant computation in response to what @38_14 said about there being no such thing as a computation that’s constant, which is, you probably agree, a bit silly. The top comment didn’t seem to be talking about constexpr specifically since the point was about computation and they said “constexpr and similar” which I assume includes const, consteval, as well as the general idea of baking the results of compute at compile time.

HN isn’t really the right place to demand justifying the existence of constexpr, but its existence has been debated at length by people who know a lot more about C++ than me. If you want to read about why it exists, there are lots of good places like cppreference.com and isocpp.org and even stack overflow.


This seems to be saying nothing. constexpr allows you to maintainably express the computation of a constant. This is useful, even if it morally is a script generating that constant it's valuable to not need to drag along a bunch of extra stuff to maintain a program.


Just what c++ is missing. More complicated builds.


It's gonna come full hilarious circle when someone announces a C++ build tool written in rust


It exists, and not just for C++: https://buck2.build/


Wow, this opinion really is unpopular. I'm glad you shared it, because I learned a lot from the replies! I wonder, after all these rebuttals, do you still hold your same unpopular opinion, or has the Vox Populi dissuaded you?


A compiler is exactly that. For example it turns the int literal 4738 into a binary representation.


Uh? Why couldn't a computation be constant?

    constexpr auto size = 4;
    constexpr std::array<int, size> arr = {1, 2, 3, 4};
    constexpr auto i = 2;
    constexpr auto x = arr[i];


you dont have a computation in your code.


    constexpr auto x = 1+1;




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: