Repository navigation
[SUGGESTION] Allow use of named operators on non-class types #430
Description
Activity
A library-based solution can read much nicer (https://cpp2.godbolt.org/z/13Gq8Gn4q):
#include <boost/hana.hpp> using namespace boost::hana; main: () = { f := :(x) -> _ = x + x; g := :(x) -> _ = x * x; a := 1; b := 2; std::cout << f(a).plus(b).g() << '\n'; }Program returned: 0 16C++ already has a way to spell operators as methods. And it already works in both C++ and Cpp2 - just inconsistently. Closing this inconsistency gap decreases the total conceptual footprint of the language, not increases it. It might increase cppfront line count, but that is not what counts.
I think this inconsistently should be fixed if there isn't any good reason behind it by either one of the following ways:
- Allow to spell operators as methods on built-in types (e.g. allow
5.operator+(10)). - Disallow to spell operators as methods on user-defined types (e.g. disallow
a.operator+(b)).
Less inconsistency leads to less pages for the language specification.
- Allow to spell operators as methods on built-in types (e.g. allow
- Can we lose the operator keyword entirely somehow? On 20 May 2023 12:27:49 Sadeq ***@***.***> wrote: I think this inconsistently should be fixed if there isn't any reason behind it by either one of the following ways: * Allow to spell operators as methods on built-in types (e.g. allow 5.operator+(10)). * Disallow to spell operators as methods on user-defined types (e.g. disallow a.operator+(b)). Less inconsistency leads to less pages for the language specification. — Reply to this email directly, view it on GitHub<#430 (comment)>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/AALUZQKCWEUMZUMS24AT7O3XHCTC3ANCNFSM6AAAAAAXY3WOIA>. You are receiving this because you are subscribed to this thread.Message ID: ***@***.***>
Good Idea. Yes. One possible option is to use
thisinstead ofoperator.For example, operator overloading inside type definition (member functions):
Xyz: type = {} Abc: type = { // Unary Prefix Operator Plus // operator+: (this) -> Abc = {} this+: (this) -> Abc = {} // Binary Operator Plus // operator+: (this, that: Xyz) -> Abc = {} this+: (this, that: Xyz) -> Abc = {} }For example, operator overloading outside type definition (non-member functions):
Xyz: type = {} Abc: type = {} // Unary Prefix Operator Plus // operator+: (a: Abc) -> Abc = {} this+: (a: Abc) -> Abc = {} // Binary Operator Plus // operator+: (a: Abc, b: Xyz) -> Abc = {} this+: (a: Abc, b: Xyz) -> Abc = {}In this way, if
thisis used within the name of declaration (e.g. the left side of:), it would be treated as a keyword similar to the meaning ofoperator.- Hmmm, if a keyword is required, operator is more expressive than this, I was wondering why the word operator was needed at all, but maybe for parsing reasons? On 20 May 2023 14:08:54 Sadeq ***@***.***> wrote: Good Idea. Yes. One possible option is to use this instead of operator. For example, operator overloading inside type definition (member functions): Xyz: type = {} Abc: type = { // Unary Prefix Operator Plus // operator+: (this) -> Abc = {} this+: (this) -> Abc = {} // Binary Operator Plus // operator+: (this, that: Xyz) -> Abc = {} this+: (this, that: Xyz) -> Abc = {} } For example, operator overloading outside type definition (non-member functions): Xyz: type = {} Abc: type = {} // Unary Prefix Operator Plus // operator+: (this) -> Abc = {} this+: (a: Abc) -> Abc = {} // Binary Operator Plus // operator+: (this, that: Xyz) -> Abc = {} this+: (a: Abc, b: Xyz) -> Abc = {} In this way, if this is used within the name of declaration (e.g. the left side of :), it would be treated as a keyword similar to operator. — Reply to this email directly, view it on GitHub<#430 (comment)>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/AALUZQKFUC3X52E4WMCVW5TXHC653ANCNFSM6AAAAAAXY3WOIA>. You are receiving this because you commented.Message ID: ***@***.***>Reacted by Jorge Canizales
By the way,
typeis a good condidate to replaceoperatortoo.For example, operator overloading inside type definition (member functions):
Xyz: type = {} Abc: type = { type+: (this) -> Abc = {} type+: (this, that: Xyz) -> Abc = {} }For example, operator overloading outside type definition (non-member functions):
Xyz: type = {} Abc: type = {} type+: (a: Abc) -> Abc = {} type+: (a: Abc, b: Xyz) -> Abc = {}And you're right.
operatoris familiar and more expressive thanthisandtypeabout the code.Can we lose the operator keyword entirely somehow?
Why? Keywords can aid readability. Avoiding or reducing keywords should not be a goal by itself without other demonstrable benefits.
“How do we make this as different as possible from C++1” is also a non-goal.
- All true points, i was just wondering if it was necessary, whether we could just allow function names to just be symbols, but thinking further I suppose operators really are special because they can be called without . or () On 20 May 2023 14:37:18 orent ***@***.***> wrote: Can we lose the operator keyword entirely somehow? Why? Keywords can aid readability. Avoiding or reducing keywords should not be a goal by itself without other demonstrable benefits. “How do we make this as different as possible from C++1” is also a non-goal. — Reply to this email directly, view it on GitHub<#430 (comment)>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/AALUZQL6MYAOMVN4MVLEWYDXHDCIJANCNFSM6AAAAAAXY3WOIA>. You are receiving this because you commented.Message ID: ***@***.***>
I think this inconsistently should be fixed if there isn't any good reason behind it by either one of the following ways:
* Allow to spell operators as methods on built-in types (e.g. allow `5.operator+(10)`). * Disallow to spell operators as methods on user-defined types (e.g. disallow `a.operator+(b)`).Less inconsistency leads to less pages for the language specification.
I think it's because they're not functions.
Suppose you can spell them.
The next inconsistency pops up: you can't take their address, because they're not functions.
This will just go on.
So take a step back and ask:
Is bridging the gap between UDTs and fundamental types a goal for Cpp2? Should it?
Try filling the suggestion template with that as theme.Note that C++20 has this for UDTs, too.
A type with onlyoperator==will have an expression using operator!=rewritten in terms of==.
Attempting to access or take the address ofoperator!=won't work on such a type.Also, I don't think we should take away the possibility of naming those functions
(yes,operators are just functions)
just for inconsistency with fundamental types.
Why? Just so I have to use library wrappers?
Why usestd::plus<my_type>whenmy_type::operator+suffices?
See https://quuxplusone.github.io/blog/2022/10/16/prefer-core-over-library/.I think it's because they're not functions.
...
Also, I don't think we should take away the possibility of naming those functions
(yes,operators are just functions)
just for inconsistency with fundamental types.I've emphased two parts from your comment. IMO that's the inconsistency I'm talking about, either they are functions, or they are not. BTW they are operators which works on both built-in types and UDTs.
Floating point division is a function with an address, on NVidia GPUs.
Not from the perspective of the C++ abstract machine.
Fair. It wouldn't be a breaking change to make them addressable, given that current C++ code cannot take those addresses. It's a bit of a fiction to present built-in functions like those the same as user-defined functions. But it can be convenient for the user. Analogously to how presenting built-in types as user-defined types is convenient when it comes to UFCS.
This depends on the view of Cpp2. Is it higher-level language than Cpp1? Does its syntax want to hide the complexity of low-level semantics? How much of low-level semantics have to be manipulated by the programmer?
- The declaration syntax of types, functions and variables are similar.
- C-style (unsafe) arrays are not supported.
- C-style (unsafe) unions are not supported.
- Built-in types are like user-defined types with UFCS.
Consider how10.function()looks likeinthas a memberfunction, but it doesn't in reality.
It seems the aim of Cpp2 is to be safer, simpler and higher-level than Cpp1. So if built-in types are like user-defined types with UFCS in Cpp2, why not support function-style operator calls on built-in types? Cpp2 can just disallow to get the address of built-in type's operators, or make it an implementation-defined behaviour within unsafe code with a warning about it.
Built-in type's operators are something similar to
constevalfunctions, it's not possible to get the address of them.Note that the motivation for my original suggestion was not about fixing an inconsistency per se. It was inspired by playing around with left-to-right expressions and seeing how infix operators break them.
Left to right evaluation makes long expressions easier to read, write and debug.
My preferred approach is to make them global template functions. For user defined types the operator method and the global function invoked as UFCS would have exactly the same effect so you need not care which one takes precedence.
This also makes the whole issue of taking their address moot.
My preferred approach is to make them global template functions. For user defined types the operator method and the global function invoked as UFCS would have exactly the same effect so you need not care which one takes precedence.
This also makes the whole issue of taking their address moot.
If we can get the address of it, doesn't it have the overhead of function call?
It'd probably be inlined everywhere. It'd also let us not have to document and explain all this family of things: https://en.cppreference.com/w/cpp/utility/functional/less
Reacted by orentIt'd probably be inlined everywhere.
So we would get the address of a function which would not be called (it would be inlined everywhere).
Exactly like any other function that's inlined in places the compiler deems appropriate. The point of getting the address to a function is not to "have an address that other people are calling". It's to call that function via de-referencing the address. If you don't call it, then the program counter might never store that value, right.
Floating point division is a function with an address, on NVidia GPUs.
I'm thinking about your comment. Isn't it better to just return the address of actual function?
Cpp2 can wrap the operation in an inline function if they don't have an actual function.
Yes I'm presuming the simplest implementation is a bunch of inline top-level-namespace functions like @orent proposed. If you want to call it with a stack frame and all and take its address you can, but otherwise the compiler can inline if it's a built-in of the platform.
Kind of like boxed types in Java or C#.
Yes I'm presuming the simplest implementation is a bunch of inline top-level-namespace functions like @orent proposed. If you want to call it with a stack frame and all and take its address you can, but otherwise the compiler can inline if it's a built-in of the platform.
Being able to inline has nothing to do with them being “built in”. Compilers inline your functions while also generating a callable version if you take their address. And in a wonderful collusion with linkers they ensure different modules will get the same same function address even if they independently include the same header or otherwise generate an identical version.
I know, but you definitely want all the arithmetic operators that are built ins of the platform to be inlined, or you'll kill performance. That's how you should read that comment.
UFCS is force-inlined, so I think it's reasonable to expect the same here.
I may have an use case for this, for a sufficiently transparent implementation of the feature.
I'm trying to convert the Cpp1 requirement
{ v++ } -> std::same_as<T>;to Cpp2 (with an implementation of #588).
The best I can do right now with just the language is{ v.operator++(0) } throws is std::same_as<T>;.
But that only works for class types, as the opening comment of this issue points out.How I wish Boost.Lambda2's operators were SFINAE friendly.
- Repository owner locked and limited conversation to collaborators
on Aug 30, 2023
Expression that flow from left to right without evaluation order jumping around are easier to read and write and less error prone. This is probably one of the reasons for the success of the shell pipe. The use of postfix operators in Cpp2 makes it easier to maintain such strict left-to-right order without requiring the use of extraneous parentheses. It also combines particularly well with UFCS.
But this does not work with the infix operators. While compatibility with math notation is important, having an alternative way to perform these operations without triggering the usual operator evaluation precedence rules would be useful.
The most natural alternative spelling for the infix operators is
.operator★(). This actually already works in C++ and therefore in Cpp2. It is possible to rewriteg(f(a)+b)as the pure left-to-rightf(a).operator+(b).g()- but it only works when the objects are class types. This does not work with ints, for example.Suggestion:
a.operator★(b)always equivalent to((a) ★ (b))Possible implementations:
.operator★()methods to the builtin non-class typesoperator★()global builtin functions that can be used either in the formoperator★(a,b)or UFCSed toa.operator★(b)The latter seems simpler and would just require translating
operator★(a,b)to standard template functionscpp2::operator_name(a,b).