Date: Thu, 13 Feb 2020 12:38:36 -0500
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_at_[hidden]>
wrote:
> On Tue, Feb 11, 2020 at 5:59 PM Joseph Malle via Std-Proposals <
> std-proposals_at_[hidden]> 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:
>
> // https://godbolt.org/z/FCf6jn
> #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
>
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_at_[hidden]>
wrote:
> On Tue, Feb 11, 2020 at 5:59 PM Joseph Malle via Std-Proposals <
> std-proposals_at_[hidden]> 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:
>
> // https://godbolt.org/z/FCf6jn
> #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
>
Received on 2020-02-13 11:46:43