Skip to content

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

Description

@orent

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

Activity

  1. JohelEGP commented on May 7, 2023

    @JohelEGP
    Contributor

    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
    
  2. orent commented on May 8, 2023

    @orent
    Author

    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.

  3. msadeqhe commented on May 20, 2023

    @msadeqhe

    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.

  4. SebastianTroy commented on May 20, 2023

    @SebastianTroy
  5. msadeqhe commented on May 20, 2023

    @msadeqhe

    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.

  6. SebastianTroy commented on May 20, 2023

    @SebastianTroy
  7. msadeqhe commented on May 20, 2023

    @msadeqhe

    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.

  8. orent commented on May 20, 2023

    @orent
    Author

    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.

  9. SebastianTroy commented on May 20, 2023

    @SebastianTroy
  10. JohelEGP commented on May 20, 2023

    @JohelEGP
    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/.

  11. msadeqhe commented on May 20, 2023

    @msadeqhe

    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.

  12. jcanizales commented on May 22, 2023

    @jcanizales

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

  13. JohelEGP commented on May 22, 2023

    @JohelEGP
    Contributor

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

  14. jcanizales commented on May 22, 2023

    @jcanizales

    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.

  15. msadeqhe commented on May 23, 2023

    @msadeqhe

    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.

  16. msadeqhe commented on May 23, 2023

    @msadeqhe

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

  17. orent commented on May 23, 2023

    @orent
    Author

    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.

  18. msadeqhe commented on May 23, 2023

    @msadeqhe

    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?

  19. jcanizales commented on May 23, 2023

    @jcanizales

    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

  20. msadeqhe commented on May 23, 2023

    @msadeqhe

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

  21. jcanizales commented on May 23, 2023

    @jcanizales

    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.

  22. msadeqhe commented on May 23, 2023

    @msadeqhe

    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.

  23. jcanizales commented on May 23, 2023

    @jcanizales

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

  24. orent commented on May 23, 2023

    @orent
    Author

    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.

  25. jcanizales commented on May 23, 2023

    @jcanizales

    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.

  26. JohelEGP commented on May 23, 2023

    @JohelEGP
    Contributor

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

  27. JohelEGP commented on Aug 15, 2023

    @JohelEGP
    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.

  28. Repository owner locked and limited conversation to collaborators on Aug 30, 2023
  29. converted this issue into a discussion #636 on Aug 30, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions