Thanks for your comments!

I think your point about double lookup is a reason not to think of this as expression rewriting.  Although the existing operator deduction methods are outdated, I think it is simpler to adjust them than to come up with new methods.

I don't want to change member vs. non member lookup.  It is weird that the language has both, but that's beyond the scope of what I want to change.


In response to your other edge cases:

Prefix/postfix operators:

I did think a little about this case before.  I don't think this is problematic.  If you specify an operator++ that takes an integer first argument, it should be a postfix operator++ regardless of whether there's a default for that integer.

    struct S {
        void operator++(){} // prefix
        void operator++(std::string s = "abc"){} // prefix
        void operator++(int n){} // postfix (no change here: n still has default 0)
        void operator++(int n = 1){} // postfix
    };


Unary/binary operators:

Great point -- This is a serious problem for this proposal.

I want lookup to be based on the non defaulted arguments.  Currently we have:

    struct S {};
    void operator*(S) {} // unary
    void operator*(S, int) {} // binary

If we allow defaults on operators, then this could occur:

    struct S {};
    void operator*(S) {} // unary
    void operator*(S, int) {} // binary
    void operator*(S, int n = 5) {} // error(?)

I think this should be an error for redeclaring a class member.  If the previous declaration is removed:

    struct S {};
    void operator*(S) {}
    void operator*(S, int n = 5) {} // OK; unary with default argument

Maybe it's weird that the unary operator* can "redeclare" the binary operator*, but I think it's much better to get a compiler error for this ambiguity rather than unexpectedly have the "wrong" function called.  I suppose this error could be relaxed later if anyone wants this done differently/if someone comes up with a better idea.

This example exposes another problem: simply adding/removing the default argument can change a operator* between binary/unary.  This is well defined and would probably lead to compiler errors rather than runtime errors, but it's ugly.  To prevent confusion surrounding multiple declarations/definitions without the defaults, I think we should require that operators with default arguments must be defined where they are declared (like friends that take default arguments).

    struct S {};
    void operator*(S) {}
    void operator*(S, int n = 5); // error

This error would be just like the error for friends: operator declaration specifying a default argument must be a definition.


Fundamental types:

    void operator+(decltype(nullptr), int, S = {}) { }  // this would be ill-formed for some reason, right?

Yes, this should be an error.  The current requirement that operators must have at least one parameter of class or enumeration type needs to change.  It should say that operators must have at least one *non defaulted* parameter of class or enumeration type.  This preserves the intention of the original rule that you cannot override operators for fundamental types.

These are great edge cases -- thank you again.  Let me know what you think of my responses/if you think of more potential pitfalls.


Btw, for source_location specifically, I believe the state-of-the-art hack (besides "don't use it, keep using your old DEBUG_LOG macros because there's nothing wrong with them, and wait for something better to come along") is to insert an implicitly constructible wrapper type. Like this:

I thought about a wrapper type.  Unfortunately, my operators' arguments are template parameters (so I hit the deduction + conversion problem) and many of the operators are binary operators which don't lend themselves well to macros, so I don't think there is another way.  Maybe I am missing something though!  I did think about using default template arguments but there is no linkage for the file and function names.

Joseph

On Wed, Feb 12, 2020 at 9:29 AM Arthur O'Dwyer <arthur.j.odwyer@gmail.com> wrote:
On Tue, Feb 11, 2020 at 5:59 PM Joseph Malle via Std-Proposals <std-proposals@lists.isocpp.org> wrote:
Currently, operators do not support default arguments in most cases and must have a predetermined number of arguments.

For example, operator/ must have two arguments and those arguments cannot have defaults.  I propose allowing such operators to have additional arguments as long as they have defaults.  The default arguments must come after the regular arguments.

auto operator/(U, V) // OK
auto operator/(U, V, W) // Error, and should remain an error
auto operator/(U, V = V(), W) // Error, and should remain an error

auto operator/(U, V, W = W()) // Error, but should be ok

I don't think operator() would need to change at all as it already supports default arguments.  It would now need wording to allow defaults in any order.  Perhaps there are other operators with special cases that I haven't thought of.

The reason I want to have this feature is to use std::source_location with operators.  As far as I can tell, default arguments are the only sensible way to use std::source_location.

This seems like a reasonable goal, and a reasonable way of getting there. But it needs a lot more fleshing out with the technical details. AIUI, essentially you're proposing to completely overhaul the ridiculous circa-1984 way that C++ does operator overloading, and replace it with something more like expression rewriting plus function overload resolution. So
    z = x / y;
would first be rewritten into
    z = operator/(x, y);  // and/or x.operator/(y)? How to specify that "double" lookup? Some of this work is done for you already; but is all of it?
and then do overload resolution to see which operator/s in scope were viable for that call.

This is a good goal. But you'll have to figure out how to specify it. In particular I can think of these corner cases:
    struct S {
        void operator++(int i=0);  // currently ill-formed
        friend void operator*(S, int i=0);  // currently ill-formed; also operators +, -, &
    };
    S s; ++s;  // can this now call s.operator++(0)?
    *s;  // can this now call operator*(s, 0)?

    void operator+(decltype(nullptr), int, S = {}) { }  // this would be ill-formed for some reason, right?
    nullptr + 2;  // this can't possibly call the user-defined operator+, can it?

 
I think this is a backwards compatible change (but perhaps it could be detected by concepts? not 100% sure).

There's no such thing as a purely backward compatible change, in the presence of SFINAE. So don't worry about that.
Worry more about how to specify the behavior you want.


Btw, for source_location specifically, I believe the state-of-the-art hack (besides "don't use it, keep using your old DEBUG_LOG macros because there's nothing wrong with them, and wait for something better to come along") is to insert an implicitly constructible wrapper type. Like this:

#include <experimental/source_location>
#include <iostream>

struct OldDebugStream {
    OldDebugStream& operator<<(const char *msg) {
        std::cout << msg;
        return *this;
    }
    OldDebugStream& operator<<(int i) {
        std::cout << i;
        return *this;
    }
};
OldDebugStream ods_;
#define ods ods_ << __FILE__ << ":" << __LINE__ << ":"

struct NewDebugStream {
    struct Annotated {
        using SourceLoc = std::experimental::source_location;
        NewDebugStream *s_;
        Annotated(NewDebugStream& s, 
                  SourceLoc loc = SourceLoc::current()) : s_(&s)
        {
            *this << loc.file_name() << ":" << loc.line() << ":";
        }
    };
    friend Annotated operator<<(Annotated a, const char *msg) {
        std::cout << msg;
        return a;
    }
    friend Annotated operator<<(Annotated a, int i) {
        std::cout << i;
        return a;
    }
};
NewDebugStream nds;

int main()
{
    ods << "Hello world! " << 42 << "\n";
    nds << "Hello world! " << 42 << "\n";
}

–Arthur