Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SUGGESTION] Allow use of named operators on non-class types #430

Closed
orent opened this issue May 7, 2023 · 27 comments
Closed

[SUGGESTION] Allow use of named operators on non-class types #430

orent opened this issue May 7, 2023 · 27 comments

Comments

@orent
Copy link

orent commented May 7, 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 rewrite g(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:

  • Make a.operator★(b) always equivalent to ((a) ★ (b))

Possible implementations:

  • Add .operator★() methods to the builtin non-class types
  • Make operator★() global builtin functions that can be used either in the form operator★(a,b) or UFCSed to a.operator★(b)

The latter seems simpler and would just require translating operator★(a,b) to standard template functions cpp2::operator_name(a,b).

@JohelEGP
Copy link
Contributor

JohelEGP commented May 7, 2023

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
16

@orent
Copy link
Author

orent commented May 8, 2023

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

@msadeqhe
Copy link

msadeqhe commented May 20, 2023

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.

@SebastianTroy
Copy link

SebastianTroy commented May 20, 2023 via email

@msadeqhe
Copy link

msadeqhe commented May 20, 2023

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+: (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 this is used within the name of declaration (e.g. the left side of :), it would be treated as a keyword similar to the meaning of operator.

@SebastianTroy
Copy link

SebastianTroy commented May 20, 2023 via email

@msadeqhe
Copy link

By the way, type is a good condidate to replace operator too.

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. operator is familiar and more expressive than this and type about the code.

@orent
Copy link
Author

orent commented May 20, 2023

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.

@SebastianTroy
Copy link

SebastianTroy commented May 20, 2023 via email

@JohelEGP
Copy link
Contributor

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 only operator== will have an expression using operator != rewritten in terms of ==.
Attempting to access or take the address of operator!= 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 use std::plus<my_type> when my_type::operator+ suffices?
See https://quuxplusone.github.io/blog/2022/10/16/prefer-core-over-library/.

@msadeqhe
Copy link

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.

@jcanizales
Copy link

Floating point division is a function with an address, on NVidia GPUs.

@JohelEGP
Copy link
Contributor

Not from the perspective of the C++ abstract machine.

@jcanizales
Copy link

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.

@msadeqhe
Copy link

msadeqhe commented May 23, 2023

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 how 10.function() looks like int has a member function, 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.

@msadeqhe
Copy link

msadeqhe commented May 23, 2023

Built-in type's operators are something similar to consteval functions, it's not possible to get the address of them.

@orent
Copy link
Author

orent commented May 23, 2023

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.

@msadeqhe
Copy link

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?

@jcanizales
Copy link

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

@msadeqhe
Copy link

It'd probably be inlined everywhere.

So we would get the address of a function which would not be called (it would be inlined everywhere).

@jcanizales
Copy link

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.

@msadeqhe
Copy link

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.

@jcanizales
Copy link

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

@orent
Copy link
Author

orent commented May 23, 2023

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.

@jcanizales
Copy link

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.

@JohelEGP
Copy link
Contributor

UFCS is force-inlined, so I think it's reasonable to expect the same here.

@JohelEGP
Copy link
Contributor

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 Aug 30, 2023
@hsutter hsutter converted this issue into discussion #636 Aug 30, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Projects
None yet
Development

No branches or pull requests

5 participants