Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Cpp2 should not use == to express constexpr #959

svew started this conversation in Suggestions
Discussion options

In short, reusing == as a way of defining that a function or variable is constexpr or consteval is a neat idea, but ultimately creates way more issues than it solves. We should not use == for this purpose, and should use keywords to color the types of these functions and variables.

I've seen this discussed a bit before, but I wanted to make a full argument against it and give room for discussion about it. I have skepticism about other areas I've seen using == for assignment, but I'm keeping my scope focused on this because I think it's most important here. Please let me know if I'm mistaken, unaware of recent developments, could look at it a different way, etc.

1. It's significantly divergent from the common use of ==

In every programming language I've ever used, == has meant one thing: An equality test. If you ask any programmer in the world what == means, they will give you that answer, no extra context needed. It's one of the most ubiquitous operators ever, and exists in almost every higher-level language.

That's a powerful thing. It means that basically no teaching has to be done whenever a programmer sees ==... except for cpp2. A programmer will look at cpp2 will be confused by why there are equality tests where it appears definitions should be occurring. In cpp2, in specific contexts, it is not an operator, it does not test equality, and it does not return a boolean. All expectations have been broken. There is now an entire class of cases where == does not mean what every programmer would expect it to mean.

2. Its role in the syntax is non-standard

When we write this code:

myfunc1: (x: int) -> int = {
 ...
}

Everything between the ":" and the "=" is the type of the thing being defined. Nothing before or after those two symbols alters the type declaration of this function. This is true for every kind of declaration or definition... except for a constexpr function:

myfunc2: (x: int) -> int == {
 ...
}

In this case, == modifies the type. The assignment operator becomes part of the type, and now myfunc1 has different type than myfunc2, and the two cannot be exchanged for one another. A new special case for type declarations has been added to the language.

3. It's unfriendly to documentation

If myfunc1 and myfunc2 have different types due to = vs. ==, how should we write the type? If I'm writing documentation for my code, or creating an intellisense tool for cpp2, how to I denote that this function is constexpr? Do I use cpp1 keywords that never show up in cpp2? Do I just write "=="? How do I write out this type information to communicate it to others?

myfunc2(x: int) -> int ==
 x: Input value for the func
 returns: Result of the calculation

4. It doesn't cover expressing both constexpr and consteval

I'll assume I don't have to explain why both are useful.

I've seen some chatter about possibly defining === as consteval, but this is... frankly horrifying to me. It is again divergent from typical uses (see javascript), and again doesn't read like it's making a statement about types, code contracts, and enforced compile-time evaluation. At that point, how about we just turn all of our function/variable coloring into unrelated symbols we shove into the assignment operator! Let's make a consteval static member function defined as x: (i) ===% { ... }, and a constexpr virtual final member function into y: (i) ==$& { ... }. /s

5. It's not extensible

If == remains the defacto constexpr tag for values and functions, the only place we can use it without causing further confusion or complication is during assignments. There is no possibility for defining a parameter to be constexpr, for defining a block consteval, or for making a consteval if statement. Cpp1 was able to reuse its keywords to add in all this new functionality that they maybe hadn't anticipated on first run, but without that sort of reusability, we're setting ourselves up to either not be able to express those things, or find weird new ways that don't connect concepts/keywords.

Conclusion

We should just use a keyword in the type declaration.

There's not really a better way of going about this. We are explicitly coloring our function or variable when we say that it's constexpr or consteval, and that's okay. We can bikeshed on what keywords we want to use and how they're defined, whether it's constexpr, consteval, const, consteval? const!, etc. etc.. Regardless, we should explicitly declare our intent in the type definition with keywords. It helps us communicate clearly what it is we're doing, it doesn't make special cases to reuse already well-defined operators, it's familiar, and it doesn't constrict us to just assignment contexts in the future if we want to expand the language.

You must be logged in to vote

Replies: 6 comments 8 replies

Comment options

I totally agree. I also find the == syntax not very intuitive.

You must be logged in to vote
0 replies
Comment options

Agreed, thank you for taking the time to express all these worries so succinctly On 28 January 2024 23:00:20 Walter ***@***.***> wrote: In short, reusing == as a way of defining that a function or variable is constexpr or consteval is a neat idea, but ultimately creates way more issues than it solves. We should not use == for this purpose, and should use keywords to color the types of these functions and variables. I've seen similar sentiments in other places too.<#714 (comment)> I've seen this discussed a bit before, but I wanted to make a full argument against it and give room for discussion about it. I have skepticism about other areas I've seen using == for assignment, but I'm keeping my scope focused on this because I think it's most important here. Please let me know if I'm mistaken, unaware of recent developments, could look at it a different way, etc. 1. It's significantly divergent from the common use of == In every programming language I've ever used, == has meant one thing: An equality test. If you ask any programmer in the world what == means, they will give you that answer, no extra context needed. It's one of the most ubiquitous operators ever, and exists in almost every higher-level language. That's a powerful thing. It means that basically no teaching has to be done whenever a programmer sees ==... except for cpp2. A programmer will look at cpp2 will be confused by why there are equality tests where it appears definitions should be occurring. In cpp2, in specific contexts, it is not an operator, it does not test equality, and it does not return a boolean. All expectations have been broken. There is now an entire class of cases where == does not mean what every programmer would expect it to mean. 2. It's role in the syntax is non-standard When we write this code: myfunc1: (x: int) -> int = { ... } Everything between the ":" and the "=" is the type of the thing being defined. Nothing before or after those two symbols alters the type declaration of this function. This is true for every kind of declaration or definition... except for a constexpr function: myfunc2: (x: int) -> int == { ... } In this case, == modifies the type. The assignment operator becomes part of the type, and now myfunc1 has different type than myfunc2, and the two cannot be exchanged for one another. A new special case for type declarations has been added to the language. 3. It's unfriendly to documentation If myfunc1 and myfunc2 have different types due to = vs. ==, how should we write the type? If I'm writing documentation for my code, or creating an intellisense tool for cpp2, how to I denote that this function is constexpr? Do I use cpp1 keywords that never show up in cpp2? Do I just write "=="? How do I write out this type information to communicate it to others? myfunc2(x: int) -> int == x: Input value for the func returns: Result of the calculation 4. It doesn't cover expressing both constexpr and consteval I'll assume I don't have to explain why both are useful. I've seen some chatter about possibly defining === as consteval<b589f5d#commitcomment-128465809>, but this is... frankly horrifying to me. It is again divergent from typical uses (see javascript), and again doesn't read like it's making a statement about types, code contracts, and enforced compile-time evaluation. At that point, how about we just turn all of our function/variable coloring into unrelated symbols we shove into the assignment operator! Let's make a consteval static member function defined as x: (i) ===% { ... }, and a constexpr virtual final member function into y: (i) ==$& { ... }. /s 5. It's not very extensible If == remains the defacto constexpr tag for values and functions, the only place we can use it without causing further immense confusion or complication is during assignments. There is no possibility for defining a parameter to be constexpr, for defining a block consteval, or for making a consteval if statement. Cpp1 was able to reuse its keywords to add in all this new functionality that they maybe hadn't anticipated on first run, but without that sort of reusability, we're setting ourselves up to either not be able to express those things, or find weird new ways that don't connect concepts/keywords. Conclusion We should just use a keyword in the type declaration. There's not really a better way of going about this. We are explicitly coloring our function or variable when we say that it's constexpr or consteval, and that's okay. We can bikeshed on what keywords we want to use and how they're defined, whether it's constexpr, consteval, const, consteval? const!, etc. etc.. Regardless, we should explicitly declare our intent in the type definition with keywords. It helps us communicate clearly what it is we're doing, it doesn't make special cases to reuse already well-defined operators, it's familiar, and it doesn't constrict us to just assignment contexts in the future if we want to expand the language. — Reply to this email directly, view it on GitHub<#959>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/AALUZQKJ2A2QNJOEOQLUNWDYQ3KADAVCNFSM6AAAAABCOQL7NWVHI2DSMVQWIX3LMV43ERDJONRXK43TNFXW4OZWGE2DKMBXGI>. You are receiving this because you are subscribed to this thread.Message ID: ***@***.***>
You must be logged in to vote
0 replies
Comment options

I upvoted your post because I mostly agree with it, specially on the toolability and extensibility part, but I also feel like I should mention the things I like from the current alias syntax, which is not that bad yet nor is it that confusing once you use it for a bit (YMMV):

  • It has the generality of being able to declare both constexpr things (which should by definition not be mutable) and actual using aliases (which are by definition not mutable), that is very nice and uniform.
  • It fits nicely with the rest of "declaring new entities with :", you can only use this "special operator" when declaring something new. (e.g.: something :== other).
  • Thanks to unambiguous parsing, it's easy to tell the cases apart when something is an expression vs something is a declaration, so in reality it is not that confusing when put into context IMO (I should say, minus the wart introduced by tersest lambda syntax where you can skip the = but I already made a post about it in the other thread 😛)

I also fail to see an alternative which doesn't involve spelling constexpr or consteval everywhere, and everyone in the thread seems to agree that we should actually be spelling that... Why? Ideally we'd like all our new code to be mostly compile-time able and to skip the ceremony when possible, personally I am tired of having to spell constexpr auto something() const noexcept -> a_type everywhere, it is very verbose and gets tedious after the first 3 functions you are attempting to write 😅 I hope we can come up with a better solution to that in this thread, if possible.

You must be logged in to vote
3 replies
Comment options

I'm less familiar with some of the points you made about consistency with aliasing and how it relates to parsing, but I definitely empathize with the point about writing constexpr everywhere. It's a problem without a good solution, in my opinion. There's no place where having constexpr be the default makes more sense than not, and it seems very difficult to infer or make sense of that inference, so we're a bit stuck with having to denote somewhere in the function signature that it's constexpr...

Comment options

Actually, maybe it is possible to infer constexpr?

A function is not constexpr if:

  • It contains references to globals
  • It defines variables with static storage durations
  • It is a coroutine
  • Uses gotos
  • If any inputs or outputs are not literal types (i.e. constexpr constructable)

It doesn't seem impossible to deterministically say whether a function is constexpr based on those rules.

All the things that would make a function an invalid constexpr function are also things that make it an impure function too. Maybe if your function isn't pure, you have to mark it with !pure. I haven't thought thru all the consequences of a thing like that, but basic idea being that I think it might actually be possible, with our principles of making information flow well defined, to determine which functions or variables being called are constexpr or not, and then maybe enforce defining them as such or simply inferring it and allowing tooling to notify you of the compilers interpretation.

Comment options

I don't think that determining constexpr-ness in Cpp2 code has ever been the problem, I imagine it would be a lot of work but I wouldn't say impossible... The real problem is that Cpp2 must transparently work with Cpp1-defined code, and there is no way we'd figure out if something defined in Cpp1 is or isn't constexpr/consteval before lowering to Cpp1 itself (that would defeat the purpose of transpiling, I imagine?). Cppfront could maybe guarantee constexpr in cases where all the definitions used within a function are in Cpp2 code, but then this would silently break as soon as you introduce a Cpp1 construct (including STL stuff), that would be no bueno.

Comment options

I don't buy 1. argument. Let me show you counter example in your own post. All programmers know that = means assignment to variable. (Well, let's put aside fact that in math it means equality...) But, later you use = as function declaration. And it comes naturally, despite that's not variable, and you cannot reassign later, but it can be tough as some "const metavariable" of compiler state to which you assign fn body.
So, it is really like that with ==. I feel its denoting comptime function close to pure function, f(x) == f(x), i.e. same result for same arg and thus can be optimized to compiletime. Not formally, not talking about stuff like constexpr random() { return TIME } stuff, but close in feel.

Also, I don't feel 4. constexpr and consteval as something great at cpp1. I have feeling that one of them not needed. I tried to search, and get idea that constexpr MAY be comptime, and consteval MUST be comptime and CANNOT be runtime, so, I'd say bye to constexpr, you don't guarantee anything, almost like old good inline... On the other hand, consteval I just don't fully understand why it's needed, they say it will be good for reflection, but I have feeling that cpp2 will have better tools for reflection.
Of course there is still need to communicate with cpp1, it's like situation where we have in parameters but still need const parameters in some cases.

You must be logged in to vote
4 replies
Comment options

On the other hand, consteval I just don't fully understand why it's needed, they say it will be good for reflection, but I have feeling that cpp2 will have better tools for reflection.

Reflection just can't be done without pure compile time functions, it's not about having a better tool in cpp2. Pure compile time functions will be needed one way or the other, either for reflection or for replacing preprocessor stuff. On the other hand, constexpr is something that isn't really needed, every function can be constexpr by default and if that isn't possible, it could just "decay" to a simple runtime function, lambdas behave this way I think. But there isn't a way I know to do this at transpiler level so constexpr cannot be get rid of.

Comment options

C++20 consteval is Cpp2 metafunctions: Cpp1 consteval came from this Cpp2 work, years before I announced Cpp2.

Some history: While Andrew Sutton and his team were implementing the Cpp2-based reflection prototype for my P0707 metaclasses paper, Andrew found that he needed a way to guarantee that a function would only run at compile time, because reflection information is only available at compile time. Using constexpr would not be sufficient, because a constexpr function could potentially execute at run time and so it could not validly access reflection information. Since Cpp1 did not have such a "function that can run only at compile time" concept, Andrew proposed (and WG21 accepted) consteval functions to fill that gap.

So Cpp1 added consteval to express Cpp2 metafunctions in a language that didn't yet have reflection and metafunctions of its own. In Cpp2, all reflections are currently used by metafunctions which are compile-time by definition, so the problem doesn't arise.

Comment options

I've mentioned that <=> was the first Cpp2-derived feature to be approved for ISO C++, and it was... <=> was adopted for C++20 at the November 2017 meeting.

But consteval was too... consteval was adopted for C++20 at the November 2018 meeting. Though, as I mentioned in the previous comment above, consteval wasn't so much directly a Cpp2 feature as it was a workaround to fill a gap in Cpp1 for Cpp1's lack of a Cpp2 feature.

Comment options

Responding to your point about #⁠1, I see what you say about = being used in a bit of a different way for function definitions, but I don't think it's actually that far from the common use of = or how it's normally conceptualized. In more dynamically typed languages (I'm thinking Javascript and Lua), one way you'd define classes is by assigning a constructor function to a variable:

// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
const Rectangle = class Rectangle2 {
 constructor(height, width) {
 this.height = height;
 this.width = width;
 }
};

Or sometimes when defining member functions of an object, you assign the object (dictionary) a field equal to a function:

-- From https://www.lua.org/pil/16.html
Account = {
 balance = 0,
 withdraw = function (self, v)
 self.balance = self.balance - v
 end
}

I think this makes sense because = has a much broader definition in people's minds than == does. = just generally means "assignment", and the use cases and mechanisms by which that happens are different in different languages. In Java, it means setting the value of a primitive or object reference (basically a pointer), and no other logic occurs. In C++ or other systems languages, it can mean construction of a small value or large data type, it can mean moving/copying with an accompanying function call, etc..

Compare that to basically every language where == is valid, in which you expect it to be an operator which takes two arguments, compares them by calling an .Equals() function or similar (or compares outright if they're basic types), and returns a boolean. Much narrower definition. That's not to say that we can't generalize it to fit our needs, but the broadest generalization of its understanding is "comparison" or "equality check/assertion", neither of which remotely come close to describing a type-modifying assignment operator like in cpp2.

Comment options

While I don't mind using == for constexpr, but if we're still exploring alternatives, perhaps we could use a special symbol as a prefix or suffix for the identifier to indicate constexpr. Some random examples for illustration:

`myfunc: (x: i32) -> i32 = { } 
myfunc`: (x: i32) -> i32 = { }
'myfunc: (x: i32) -> i32 = { }
myfunc': (x: i32) -> i32 = { }
 ...

I'm not sure which symbol, prefix, or suffix would work best, but this approach is less verbose than explicitly writing constexpr. What do you think?

You must be logged in to vote
1 reply
Comment options

This would technically be an improvement on the current situation, but I think unfortunately the majority of issues I identified with == would still exist if we switched to '

Comment options

Just to mention another alternative, although it is probably a very crazy idea:

cpp2 could also change the equality comparison operator from == to something else, for example =?.
Then == would mean to enforce or "strengthen" the assignment (by using constexpr) and the comparison operator would look very different.

I am mainly coming up with this idea, because in my (very limited) experience, newbies or junior programmers have troubles with the comparison operator. It isn ́t intuitive at all, at first sight. I think, when people start to learn a programming language, they often tend to write something like if (a = b) instead of if (a == b). However, I am not a teacher and I don't have numbers or any evidence (so I could be totally wrong).

On the other hand, I also want to add some critical thoughts about this idea as well:

  • I am not sure, that if (a =? b) would improve the situation at all.
  • Furthermore, it is probably targeting only a very minor issue. Might not be worth it.
  • The simple fact, that a lot of other programming languages use == for equality comparison is also a strong argument against using something like =?.

Therefore, it is probably just a crazy idea and nothing more.

You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

AltStyle によって変換されたページ (->オリジナル) /