Hacker News new | ask | show | jobs
by mhh__ 1118 days ago
`if constexpr` is such a disaster. They were so close to getting it right (not introducing a scope) but they missed.

Similarly constexpr itself is also genuinely ridiculous: (I have said this on hackernews before) It's such a stupid idea to require an annotation everywhere you want to evaluate things at compile time, practically everything will inevitably be evaluatable at compile time, and you need the implementation anyway, so just let it fail rather than ask for permission everywhere.

Having the keyword for variables and constants is fine (i.e. top down and bottom up constraints need to be dictated) but you shouldn't need to write constexpr more than that.

10 comments

> It's such a stupid idea to require an annotation everywhere you want to evaluate things at compile time, practically everything will inevitably be evaluatable at compile time

And making constexpr-ness an explicit contract makes sense to me: if it's not that it can be an unexpected property, and can break at any change of implementation.

Yes requiring a function being marked requires that the implementor do it, but it also means they have considered this use-case and made it officially part of the API. It's not a trivial promise to make.

> and you need the implementation anyway, so just let it fail rather than ask for permission everywhere.

"Let it fail" is the issue, if it's implicit a user can assume this is working by design, then find their program stops compiling on the next release not because the maintainer wilfully broke the API, but because they changed an implementation detail of the function and constexpr-ness is not something they considered (or assumed correct) in the first place.

Maybe the language should work the other way around and everything should be constexpr by default and functions should opt out of constexpr-ness, but that's 40 years too late for C++. And I can't think of any langage which does that. And frankly it feels like the wrong default for the reasons above.

Except constexpr isn't a guarantee. Compilers can choose to silently evaluate constexpr things at runtime. And I have ran into this before: a compiler with a recursion limit causing it to bail on constexpr and just emit the code.

So constexpr isn't a guarantee of it being evaluated at compile time, and non-constexpr isn't a guarantee of it being evaluated at runtime. Cool, huh?

> constexpr isn't a guarantee of it being evaluated at compile time

constexpr is a guarantee that you can use the thing in a constexpr context, and this is where the "evaluated at compile-time" guarantee can come from:

    template<typename T>
    auto func() {
      // here some compilers can still choose to evaluate x at run-time - and very likely all of them if no optimizations are enabled
      constexpr int x = f(); 

      // but here it becomes mandatory for this use of x to be evaluated at compile-time, since the number is literally going to be part of the compiled binary as part of the function name mangling
      return std::integral_constant<int, x>{};
    }
Constexpr does not guarantee that a constexpr function can be called at compile time, unfortunately. Only that it can be called at runtime for a subset of all possible parameters.

Except of course the subset of parameters for which it is constexpr callable can't be checked at function definition time (if it exists at all), only at function invocation time.

Which makes the constexpr annotation useless and it is in only because the authors couldn't otherwise get the paper through some committee objections.

Is such a mangling mandated by the standard?
Mangling is mentioned in the standard.

In this case though the underlying reason is that its part of the type (system) not because of the mangle specifically.

> Mangling is mentioned in the standard.

Forgive me, but can you be clearer than "mentioned"? Is the mangling required to contain template parameters for return types?

> In this case though the underlying reason is that its part of the type (system) not because of the mangle specifically.

I'm not sure. The compiler knows it will always be the same type, so under many uses of this function I could easily imagine a compiler that doesn't actually fill in .value until runtime.

Mostly makes sense to me: constexpr means the function is evaluatable at compile-time, being callable at runtime is part of the contract and why adding constexpr does not change the API.

For an assertion that something is evaluated at compile time I’d assume a variable-level annotation (in a non-constexpr function). Though I don’t know c++ enough to have any idea whether that’s the case.

And contant evaluation optimisation has always been a thing so it’s not really surprising.

> Mostly makes sense to me: constexpr means the function is evaluatable at compile-time, being callable at runtime is part of the contract and why adding constexpr does not change the API.

The problem isn't that you're able to call it at runtime with runtime data, it's that if you give it compile-time data you have no idea when it will be run.

> For an assertion that something is evaluated at compile time I’d assume a variable-level annotation (in a non-constexpr function). Though I don’t know c++ enough to have any idea whether that’s the case.

Oh, were you under the impression that constexpr was just for functions? It applies to variables too, and it's not a guarantee on them. You need to use other, newer annotations.

C++20 has consteval and constinit to fix this (at the cost of making things even more complicated).
Amazing.
> I can't think of any langage which does that

D does. And it's a very popular feature. In fact, D goes even further - only the path taken through a function needs to conform when running it at compile time, not 100% of the function.

> it feels like the wrong default for the reasons above.

In about 16 years of very extensive use, it has never been brought up as a problem.

The other problem with implicit constexpr is that it can change the timing characteristics of your code depending on whether your input data is known at compile time. And then if you want it to be constexpr you have to troubleshoot why it sometimes isn’t. I think this approach of maximal control in exchange for some foot guns is very much in keeping with C++.
In D you don't have to troubleshoot it. The compiler will tell you where the problem is if it can't be evaluated at compile time.

> for some foot guns

There just aren't any with this feature.

> Maybe the language should work the other way around and everything should be constexpr by default and functions should opt out of constexpr-ness, but that's 40 years too late for C++.

That's how lambdas behave in C++, so it's definitely feasible. gcc has a flag, -fimplicit-constexpr I think, that does this for regular functions, and it doesn't appear to cause any significant issues. I think there has been some talk around making this the language behavior at some point in the future.

> It's not a trivial promise to make.

It's actually very trivial. 99% of code I write can be run at compile time.

> maintainer wilfully broke the API

Did they break the API or did they change the semantics? The API is tittle-tattle, the point is that you cannot run it at compile time anymore (e.g. accidentally introduced a opaque new dependency by accident? Oops)

> And I can't think of any langage which does that

Being able to opt out is not what you want, but D allows everything to at least attempt to be run at compile time. This has been very profitable.

> 99% of code I write can be run at compile time.

Good for you, you missed the point.

> Did they break the API or did they change the semantics?

What are you even on about?

> The API is tittle-tattle

What are you even on about, part 2.

> the point is that you cannot run it at compile time anymore

Yes, the API allowed one thing, now it does not. That's no different from changing an optional parameter to mandatory or any other thing which breaks the API.

> Good for you, you missed the point.

>> Did they break the API or did they change the semantics?

> What are you even on about?

Constructive.

constexpr is great. I can finally do most of the compile time computation that previously required template metaprogramming, and it's much more readable by comparison. C++17 makes it much more ergonomic to use too over C++11. I can't wait for us to finally upgrade our toolchain to take advantage of C++20.

If you're in embedded and you're not pushing everything you can into constexpr, you're missing out on correctness and code size benefits.

Doing stuff at compile time is good (although the compiler can do 99% of it anyway its nice to have guarantees), C++ just got it wrong.
It could certainly be better, but in a world where my options are C, C++, or assembly, I pick C++ every day.

I do keep hoping for something else to get big enough we can use it because C++ sucks, but is still better than the alternatives we can use.

Virgil doesn't have all the backends for the diversity of embedded ISAs out there, but the first version compiled to C and ran in as little as dozens of bytes of memory (on 8-bit AVR). Nowadays I am not doing embedded systems, otherwise I'd write more backends.
C++ is much better than C. Personally I use D everywhere I would've previously used C++.
> `if constexpr` is such a disaster.

to me it's been a very useful tool for reflection, for instance

    if constexpr (requires { foo.someMember; }) { 
      use(foo.hasSomeMember);
    } else { 
      some_fallback_case();
    }
This is "design by introspection". It works a lot better if you can

1. Do this in types. 2. Do this without introducing a new scope inside a function.

I would absolutely hate `constexpr` to be implicit and left to be decided by compiler.
How D decides it is if the grammar says its a constant-expression, whatever makes up the constant-expression gets evaluated at compile time - functions and all. For example,

    int square(int x) { return x * x; }

    static assert(square(3) == 9);
This allows one to write unit tests that are checked at compile time, whereas:

    int main() { assert(square(3) == 9); }
is always a runtime check.

Compile time unit testing is a significant win:

1. it's always more productive to find problems at compile time rather than run time

2. it isn't necessary to conditionally turn off compilation of the tests for the release build

3. you can't forget to run the tests before shipping

What if it was always allowed to do compile time, but you could add a keyword to assert it did (like force-inline).

That way you get benefits by default. Whereas right now it's not allowed to.

These are two completely different things.

One is the provider of the API promising that the function supports compile-time evaluation.

The second is the consumer of the API ensuring that an expression has been compile-time evaluated. It's nice that the consumer can make sure, but that's not helpful if the provider of the API never specifically intended for a function to be evaluatable at compile-time.

> Whereas right now it's not allowed to.

Of course it's allowed, but you're at the mercy of the compiler's decisions.

> Of course it's allowed,

It's not. If you to use a function in a constexpr context then it will complain it needs to be marked as such. So the API creator needs to label every function in case a client wants to use it.

It is decided by the compiler.

Note that I mean at the declaration not the callsite.

Disaster? Just because it introduces a scope? Please elaborate. A scope is the right thing to do, every other {} introduce a scope, so if this wouldn't have been consistent with the normal 'if' that would have been extra confusing.

Maybe you'd like to do something like:

    template<typename T>
    struct ABC {
       int x;
       int y;
       if constexpr (is_3d<T>) {
          int z;
        }
     };
But yeah, that's not what if constexpr does.
That’s exactly what it could have allowed, and become a much more powerful construct in the language
Which is why it's crap => back to macros we go.
They should just skip to the end and make the entire language legal to execute at compile-time. Virgil does this and many Lisps before it. You then get a heap against which the entire program can be optimized at compile time. C++ is slowly catching up to Virgil I for microcontrollers, ala the 2006 paper.
The reason why you want it to be part of the API contract is to avoid future breaks.

If you publish a library as "constexpr" you are indicating to your users that they can use it in a compile-time context and that future changes to your implementation will remain compile-time executable. If you just say "anything that can be computed at compile time gets auto-deduced to constexpr" then you rely on some library that is compile-time executable by coincidence but you really really really need it to be compile-time executable. Now when that library owner makes an edit that means it cannot be executed at runtime your code breaks.

The example in the article is a little weird, too. Putting aside the weirdness of the intended purpose of the example, what's wrong with function overloading? I think the overloaded version is a lot easier to read. With the `if constexpr` version, now I have to edit the one function with new cases if I add a type that doesn't fit the "number or string" pattern, whereas with overloading I just implement a new `length` function.
The example is weak but a good thing to look into is https://dconf.org/2017/talks/alexandrescu.pdf i.e. this type of compile time branching (when implemented properly as mentioned above) lets you write code that "reacts" to other code in the project's capabilities. Allows some very nice patterns within a C++ flavoured type system.
>Having the keyword for variables and constants is fine (i.e. top down and bottom up constraints need to be dictated) but you shouldn't need to write constexpr more than that.

This.

Constexpr should have been at the eval site, i.e something like:

  consteval auto x = foo();
And foo() is just a normal function, if the compiler can eval it at compile time - all good - if not, compiler error.
I wish constexpr actually asked the compiler to evaluate an expression at compile time and not just a statement.
In D, every part of the grammar that is a constant-expression must be compiled at compile time. For example:

    void test() {

    int square(int y) { return y * y; }
    int x = square(3);  // evaluate at square at run time
    enum y = square(3); // evaluate at square compile time

    }
(Although, when the optimizer gets through inlining square(), etc., it will wind up at compile time anyway.)
I think a compliant c++ implementation is allowed to do this and I think all do so at least for straightforward cases.
It's allowed to but you aren't allowed to assume.