Partial fix for issue# 9769 (templated opEquals)#1439
Partial fix for issue# 9769 (templated opEquals)#1439jmdavis wants to merge 1 commit intodlang:masterfrom
Conversation
25b5475 to
bdeacdc
Compare
src/object.d
Outdated
|
|
||
| version(unittest) private void _testOpEquals2(T, U, V)(bool expected, V lhs, V rhs, size_t line = __LINE__) | ||
| { | ||
| _testOpEquals!(T, T)(expected, lhs, rhs, line); |
There was a problem hiding this comment.
Ow come on use nested static foreach:
foreach(T1; AliasSeq!(T,U, const T, const U))
foreach(T2; AliasSeq!(T,U, const T, const U))
{ ...
}
|
@jmdavis Ping! |
bdeacdc to
612f257
Compare
|
Okay. I fixed the first one so that |
612f257 to
4f40d10
Compare
15a3ce8 to
39aea0b
Compare
|
Okay. I updated the PR to fix the merge conflict, and I added the full set of attributes onto the constructors and |
|
LGTM, but I agree with Dmitry's suggestion to use foreach loops in the tests. |
|
Is the goal here only to templatize the opEquals global function? For some reason I thought that the end goal was to templatize the virtual method on the Object class. Was I mistaken? |
I really don't see how. As I said before, while the comparisons themselves may be combinatorial, the results are not.
The end goal is to remove Templatizing the free function, |
| @@ -155,15 +164,269 @@ auto opEquals(Object lhs, Object rhs) | |||
| return lhs.opEquals(rhs) && rhs.opEquals(lhs); | |||
There was a problem hiding this comment.
When object.opEquals is removed, how do you ensure that this doesn't result in a recursive call to this template again?
There was a problem hiding this comment.
Hmm. Good catch. There probably should be a check that the type has opEquals. Certainly, we're not trying to use UFCS for anything here, and having it kick in would be problematic.
There was a problem hiding this comment.
Okay. It now checks that both of the types being compared have a member named opEquals in addition to checking that the result of calling it is bool like it did before.
There was a problem hiding this comment.
It's been a long time, but I looked over this again and I think this could still result in a recursive call.
class Foo
{
bool opEquals(int x);
}In this case, Foo has an opEquals member, however, if you call foo1.opEquals(foo2), it will recursively call opEquals here. I think we could force it to call the classes opEquals function with this:
return __traits(getMember, lhs, "opEquals")(rhs) && __traits(getMember, rhs, "opEquals")(lhs);There was a problem hiding this comment.
Update. I tried to see if this would in fact result in a recursive call and it didn't seem to. My guess is that if an object contains the member opEquals, and you call obj.opEquals, then it will ALWAYS call the member function even if the arguments are invalid (it will never fall back to calling a UFCS method). Can anyone confirm this? I attempted to find this in the spec but was unsuccessful.
There was a problem hiding this comment.
It is my understanding that UFCS is only ever attempted if there is no member function with that name. The spec implies that when it says
When UFCS rewrite is necessary, compiler searches the name on accessible module level scope, in order from the innermost scope.
but it isn't very specific about it. There was a complaint about it recently in D.Learn, because someone wanted to overload a range's front with a front function that took arguments, and it didn't work. So, all the evidence that I've seen seems to indicate that UFCS is only attempted when it needs to be attempted, which presumably reduces symbol look-up times (since it only has to look at the member functions if there's one with that name), and it avoids hijacking when someone screws up calling a member function.
There was a problem hiding this comment.
Created an issue to clarify this behavior in the spec https://issues.dlang.org/show_bug.cgi?id=18523
So long as this is the correct behavior, the change in the PR looks correct.
39aea0b to
627bd45
Compare
|
Is there no way that the |
It's there to ensure commutativity, and the code immediately above it (which doesn't show up in the diff) only needs Andrei explains the reasons for why |
| if (is(T == class) && is(U == class) && | ||
| __traits(hasMember, T, "opEquals") && | ||
| __traits(hasMember, U, "opEquals") && | ||
| !is(typeof(lhs.opEquals(rhs)) == bool) && |
There was a problem hiding this comment.
Is the ! character supposed to be here?
There was a problem hiding this comment.
Yes. The whole point of this horrific overload is to cast away const when opEquals is called on a const object to allow for const objects to be compared. It's a hack that opens the door for badly behaving opEquals implementations breaking const, but without it, there's no way to compare const objects, because Object's opEquals isn't const.
With the tempaltized opEquals, if the opEquals declared on a base class is const, then it'll call the main opEquals overload without needing this one, and const won't be cast away. And when opEquals on Object is deprecated, we can deprecate this overload. Because lhs and rhs are const, the ! means that this will only compile if the member function opEquals being used isn't const. In the long run, once this overload is gone, such a comparison won't even be legal, but we can't do that as long as the opEquals on Object is still what's being used.
|
Pinging @WalterBright because this is potentially related to |
627bd45 to
513a810
Compare
wilzbach
left a comment
There was a problem hiding this comment.
I just went through the history of this PR and its predecessors and it's really a shame that after several years we are still no step closer to const opEquals or (@safe opEquals).
I don't see anything blocking this and heck, it's only a "partial" fix with a few hacks, but we need to start somewhere...
The only thing (apart from the two nits) is that this change could be accompanied by a changelog entry, but then again, it might be a good idea to wait until it's fully fixed (and the other opCmp and friends have been fixed too)
src/object.d
Outdated
| return opEquals(cast()lhs, cast()rhs); | ||
| } | ||
|
|
||
| version(unittest) private void _testOpEquals(T, U)(bool expected, T lhs, U rhs, size_t line = __LINE__) |
There was a problem hiding this comment.
We have made bad experiences with version(unittest) in the past and I think Andrei doesn't really like version(unittest) anymore. There was an effort for StdUnittest (and CoreUnittest), but it seems that didn't work out either, so I suggest to move this into the unittest block.
There was a problem hiding this comment.
When I originally implemented this, I don't think that it worked to have templated functions inside a unittest (IIRC, it only worked with one instantiation, making it pointless). Fortunately, that's been fixed. I'll update it accordingly.
This makes it so that the global opEquals function is templated. It is therefore now possible for classes to implement opEquals without overiding Object's opEquals method, including giving it different attributes (such as const). However, the hack version of the global opEquals which takes const Objects and casts away const has been left for the moment, and the opEquals on Object has not been touched. At minimum, before the hack version can go away, work will need to be done on the other classes in object.d to make them handle const properly. Also, the opEquals on Object will probably have to go away before that if we don't want to break code, as it's the only reason that const Objects have been able to be compared before now, and until types stop using Object's opEquals, that's still the only way that they'll be able to be compared. But at least with these changes, it'll be possible for classes to be compared without it if they change their signatures for opEquals to not take Object.
513a810 to
da50b8c
Compare
|
Testers are green. @andralex let's pull the trigger. |
|
I have a different plan for solving this problem, on my long "DIPs to write" list. I'll summarize it below. Currently
A key observation is that these two roles need not be fulfilled by the same type. And here comes the idea - we introduce Going the |
|
I really like the idea. I'm not seeing the connection between the name "ProtoObject" and "the root of all objects". Heres some other ideas for names because you know we got to make that "bike shed" look pretty:) |
|
I'm certainly up for helping to work on a DIP for a replacement for In principle, I'd actually like to see something like Depending on what we wanted to do, what this PR does could actually still be part of the solution, but it's just one small step in what needs to be done to fix the overall problem. So, even if the plan were specifically to remove |
|
@marler8997 per https://www.merriam-webster.com/dictionary/Prot: "a: first in time (protohistory)
(This must be the longest sentence I've read in a while. If they put their minds together, Marcel Proust and David Foster Wallace would still have nothing on you.) The DIP would not be for replacing |
| * Returns true if lhs and rhs are equal. | ||
| */ | ||
| bool opEquals(T, U)(T lhs, U rhs) | ||
| if (is(T == class) && is(U == class) && |
There was a problem hiding this comment.
if should be flush with the left edge
| { | ||
| // If aliased to the same object or both null => equal | ||
| if (lhs is rhs) return true; | ||
| static if (is(T : U) || is(U : T)) |
There was a problem hiding this comment.
static if (is(typeof(lhs is rhs)) is more direct and handles cases like comparing an immutable object with a mutable object.
There was a problem hiding this comment.
But it doesn't handle when one type is derived from another.
| // any user code using const objects but which doesn't define opEquals such | ||
| // that it works with const with the other overload will also break once | ||
| // this is removed. So, we need to get rid of this, but we need to be | ||
| // careful about how and when we do it. |
There was a problem hiding this comment.
Has the option of adding a const overload of opEquals been explored?
There was a problem hiding this comment.
I don't remember. The decision had been to remove opEquals and the other three functions entirely. It's just that we haven't been able to do that, because the built-in AAs use them. The work to templatize them has to be finished first. The other issue of course is that while we decided that the way to fix the attribute problems with these functions was to remove them from Object, almost no work has actually gone towards that. AFAIK, this and the work that's been towards fixing the AAs is it. It was a decision made without the work to back it up. And while I originally did the work for this PR years ago, it was blocked due to a compiler bug, and once that was fixed, and I recreated the PR, it just sat here. Adding a const overload wouldn't solve the overall attribute problem to allow classes to define these methods with whatever attributes were appropriate, and we were trying to solve that, not const specifically. As such, I'm not sure that overloading on const was ever really considered, though since one of the goals was to allow for classes to be able to have a non-const opEquals so that they could do stuff like lazy initialization, adding a const overload didn't really fix the problem even when just looking at const.
If we ignore the overall attribute problem and just look at const, then adding an overload for opEquals which is const sort of fixes the problem but sort of doesn't (even ignoring the issue of allowing classes to lazy initialize members). If a type just overrides the const overload, and == or != is used on a mutable object, then the mutable overload in Object would be called and not the const override. The same goes if a mutable override was declared, and a const object were used (in that case, you'd just be using the wrong overload instead of breaking the type system like we do now). Making the mutable Object.opEquals call the const one would fix the case where a derived class just declared the const override, but it wouldn't fix the case where the derived class just declared the mutable override. If we combined that with this template solution, then as long as folks weren't comparing Object, that wouldn't be a problem, because the derived class versions would be called directly, but any time that someone (like the current AA implementation) compared Objects, then there would be bugs. The result of that approach might be better than what we have now, because it wouldn't break the type system, but it would be buggy for any class that didn't override both overloads of opEquals, and it leaves the problem with allowing stuff like lazy initialiazation, but to fix that, opEquals has to be removed from Object, or we need a different root object for such a class. So, if we add ProtoObject, adding a const overload to Object then just solve the undefined behavior problem, but it causes other bugs unless the compiler forces you to override both overloads, which would be annoying but at least wouldn't be buggy.
The other question is how to then go about deprecating the current behavior. We'd basically have to add some sort of deprecation warning to the compiler about it, and then later change druntime, and break any code that hadn't been updated to have a const overload of opEquals. But if we were adding that plumbing to the compiler to give a deprecation warning, we could just make it permanently give an error about overriding only one of the two overloads. That's not exactly pretty given that in principle, it should be possible to only declare the const version if you don't need them to be different, but it would solve the problem with undefined behavior while making it possible to compare const Objects.
There was a problem hiding this comment.
The most effective is to go with a DIP on ProtoObject and then make only non-breaking improvements to Object. ProtoObject essentially makes Object legacy code so its issues become moot.
Yes. I get that it would be for adding a type underneath it, but if that were done, it then becomes possible to consider removing
It casts If we go forward with the |
Okay. I'm taking another stab at this. Previously, compiler bugs made this not work, but at least some of those have been fixed, and it's working for me locally on FreeBSD. Hopefully, the autotester won't choke on it on Windows or whatnot.
This makes it so that the global opEquals function is templated. It is
therefore now possible for classes to implement opEquals without
overiding Object's opEquals method, including giving it different
attributes (such as const).
However, the hack version of the global opEquals which takes const
Objects and casts away const has been left for the moment, and the
opEquals on Object has not been touched. At minimum, before the hack
version can go away, work will need to be done on the other classes in
object.d to make them handle const properly. Also, the opEquals on
Object will probably have to go away before that if we don't want to
break code, as it's the only reason that const Objects have been able to
be compared before now, and until types stop using Object's opEquals,
that's still the only way that they'll be able to be compared. But at
least with these changes, it'll be possible for classes to be compared
without it if they change their signatures for opEquals to not take
Object.