www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Why is `opequals` for objects not ` safe` by default?

reply Liam McGillivray <yoshi.pit.link.mario gmail.com> writes:
I am rather new to D, and have just began using the ` safe` and 
` nogc` attributes. I have just begun using FLUID, a GUI library 
that requires safe delegates to be passed to it's objects.

While writing in ` safe` mode has mostly been not very difficult, 
and I usually have a sense of why the compiler rejects what it 
rejects, I was quite surprised when the compiler rejected an 
`opEquals` between two class objects. This is the default 
`opEquals` function which from my understanding simply compares 
the two memory addresses.

I don't see why this would ever be considered unsafe, even if the 
class isn't marked as ` safe`. Is doing this operation with a 
` safe` class really any more or less risky than doing it with a 
` system` class?

Is this just an oversight, or was this a deliberate choice?
Apr 04
next sibling parent reply Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
On Thursday, April 4, 2024 11:17:07 PM MDT Liam McGillivray via Digitalmars-d 
wrote:
 I am rather new to D, and have just began using the ` safe` and
 ` nogc` attributes. I have just begun using FLUID, a GUI library
 that requires safe delegates to be passed to it's objects.

 While writing in ` safe` mode has mostly been not very difficult,
 and I usually have a sense of why the compiler rejects what it
 rejects, I was quite surprised when the compiler rejected an
 `opEquals` between two class objects. This is the default
 `opEquals` function which from my understanding simply compares
 the two memory addresses.

 I don't see why this would ever be considered unsafe, even if the
 class isn't marked as ` safe`. Is doing this operation with a
 ` safe` class really any more or less risky than doing it with a
 ` system` class?

 Is this just an oversight, or was this a deliberate choice?
The functions on Object don't have attributes, because they existed before the attributes were added to the language. In addition to that, as soon as you put attributes on the base class function, you're putting restrictions on the derived classes, and we really don't want to force a particular set of attributes on every D class in existence. Ideally, Object wouldn't have functions like opEquals, and they'd only be added by derived classes so that programs could put whatever attributes on them make the most sense, but that's a breaking change, so it hasn't happened. However, derived classes _can_ put more restrictive attributes on the functions when they override them, since that's adding extra restrictions rather than loosening them, so you can make opEquals on your derived class safe. Also, when you use == with class references, it doesn't directly call opEquals. Rather, it calls the free function, opEquals, from object.d which does some additional checks (like whether the two references are the same or whether they're null) before calling the member function, opEquals, on the references (and it won't call the member function if it's not necessary). And when it calls the member function on the references, if they're the same class, it'll call whatever opEquals the derived class has rather than the one on Object, so overloads such as those that take something other than Object can be called, giving you more control over what opEquals looks like on your class so long as you're comparing references of the same type. Of course, if the class types don't match, then the Object version will be called instead, but in that case, it'll also make sure that both lhs.opEquals(rhs) and rhs.opEquals(lhs) are true, which fixes some subtle bugs that you can get when comparing base classes and derived classes. In any case, yes, we'd like to improve the situation with Object, but it's very difficult to do so in a way that doesn't break existing code, which is why we haven't been able to fix some of its issues as well as we'd like. - Jonathan M Davis
Apr 04
parent reply Liam McGillivray <yoshi.pit.link.mario gmail.com> writes:
If you were willing to do breaking changes, perhaps the base 
`opEquals` function can be set to ` safe`. Of course, that would 
be a breaking change if anyone has an override that isn't 
compatible with ` safe`. However, perhaps there can be a 
deprecation warning for any non-safe code defined inside 
`opEquals`, and then a few years later the base function can be 
declared as ` safe`. Given that ` trusted` exists, this would be 
a very easy fix for anyone to make.

Does it currently work if a ` safe` declaration is added for 
`opEquals`, but no definition? If not I think this should work. 
It would just default to the existing `opEquals` function, but 
with the ` safe` attribute.

Anyway, I discovered that `==` can be replaced with `is`, and it 
seems to do the same thing while working within a ` safe` 
function.

On Friday, 5 April 2024 at 05:56:05 UTC, Jonathan M Davis wrote:
 Ideally, Object wouldn't have functions like opEquals, and 
 they'd only be added by derived classes so that programs could 
 put whatever attributes on them make the most sense, but that's 
 a breaking change, so it hasn't happened.
 - Jonathan M Davis
That sounds like a horrible idea. For something as basic as opEquals, one shouldn't need to do an operator override.
Apr 07
parent reply Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
On Sunday, April 7, 2024 3:02:58 AM MDT Liam McGillivray via Digitalmars-d 
wrote:
 If you were willing to do breaking changes, perhaps the base
 `opEquals` function can be set to ` safe`. Of course, that would
 be a breaking change if anyone has an override that isn't
 compatible with ` safe`. However, perhaps there can be a
 deprecation warning for any non-safe code defined inside
 `opEquals`, and then a few years later the base function can be
 declared as ` safe`. Given that ` trusted` exists, this would be
 a very easy fix for anyone to make.
No, we really don't want to force safe on opEquals, because that means that you can't write one that's system, meaning that you're potentially forced to use trusted on code that really shouldn't be treated as safe. In many cases, safe makes sense, but it doesn't always. Ideally, we wouldn't force _any_ attributes on opEquals, opCmp, toHash, or toString, and they would be left entirely up to programmers to decide. But given the nature of inheritance, that means not having them on Object and instead putting them on the derived class. And it's already the case that if you're doing anything with inheritance, you're designing your own class hierarchy with your own set of requirements - which could include attributes like safe or pure, or it could exclude them. So, it would simply mean that you couldn't compare Objects, which is just fine, since that isn't going to be a meaningful comparison unless the two class objects are from the same class hierarchy, and as long as the appropriate druntime code is templatized, it can handle whatever attributes you put on those functions in your class hierarchy instead of needing to do anything with Object.
 Does it currently work if a ` safe` declaration is added for
 `opEquals`, but no definition? If not I think this should work.
 It would just default to the existing `opEquals` function, but
 with the ` safe` attribute.
Adding declarations with no definitions is just going to result in linker errors when code tries to call the function - or nothing if the function is never called. No, if you want to add more attributes onto opEquals and actually be able to take advantage of them with ==, then you need to provide a new overload. E.G. if you have class C { bool opEquals(C rhs) safe { ... } override bool opEquals(Object rhs) safe { ... } } then the first overload will be called when comparing class references of type C - or class references derived from C, whereas if you're comparing class references of type Object (or class references which aren't C or derived from C), then they'll be compared as Object, and the base class opEquals will be called, resulting in your second overload being called thanks to polymorphism, but because the base class version is system, == will still be treated as system in that case. E.G. C lhs = new C; C rhs = new C; // Can be used in safe code, because they're both C. auto result == lhs == rhs; but Object lhs = new C; Object rhs = new C; // Can't be used in safe code, because they're both Object. auto result == lhs == rhs;
 Anyway, I discovered that `==` can be replaced with `is`, and it
 seems to do the same thing while working within a ` safe`
 function.
The is operator and == are _not_ the same. When you use is on class references, it's true if and only if the two references point to the same object. It's basically doing a pointer comparison. In contrast, == calls the free function, opEquals, which will call opEquals on the class references to compare them if they're not null and they're not pointing to the same object. And then whether they're considered equal or not depends on the implementation of opEquals. https://dlang.org/spec/expression.html#identity_expressions So, if you had something like class C { private int _value; this(int value) { _value = value; } bool opEquals(C rhs) safe { return _value == rhs._value; } override bool opEquals(Object rhs) { if(auto c = cast(C)rhs) return _value == c._value; return false; } } auto a = new C(42); auto b = new C(42); then assert(a is b); would fail, whereas assert(a == b); would be pass.
 On Friday, 5 April 2024 at 05:56:05 UTC, Jonathan M Davis wrote:
 Ideally, Object wouldn't have functions like opEquals, and
 they'd only be added by derived classes so that programs could
 put whatever attributes on them make the most sense, but that's
 a breaking change, so it hasn't happened.
 - Jonathan M Davis
That sounds like a horrible idea. For something as basic as opEquals, one shouldn't need to do an operator override.
You already need to do an override to get opEquals unless you want it to just compare the references, which is borderline useless. All that opEquals on Object does is compare the addresses of the references with the is operator, and that's almost never what you want when doing an equality check. In almost all cases, you want to be comparing the values of the member variables, and that means writing your own opEquals. - Jonathan M Davis
Apr 07
parent reply Liam McGillivray <yoshi.pit.link.mario gmail.com> writes:
On Sunday, 7 April 2024 at 09:59:56 UTC, Jonathan M Davis wrote:
 No, we really don't want to force  safe on opEquals, because 
 that means that you can't write one that's  system, meaning 
 that you're potentially forced to use  trusted on code that 
 really shouldn't be treated as  safe.
Well, I already ended up putting ` trusted` in a function I didn't want to put it in because `opEquals` was ` system`. Point taken though.
 ```
 class C
 {
     bool opEquals(C rhs)  safe
     {
         ...
     }

     override bool opEquals(Object rhs)  safe
     {
         ...
     }
 }
 ```
 then the first overload will be called when comparing class 
 references of type C - or class references derived from C, 
 whereas if you're comparing class references of type Object (or 
 class references which aren't C or derived from C), then 
 they'll be compared as Object, and the base class opEquals will 
 be called, resulting in your second overload being called 
 thanks to polymorphism, but because the base class version is 
  system, == will still be treated as  system in that case. E.G.
Oh! Is this how the `opEquals` function used can be made to depend on the reference type, rather than the type of the object contained in the reference?
 Anyway, I discovered that `==` can be replaced with `is`, and 
 it seems to do the same thing while working within a ` safe` 
 function.
The is operator and == are _not_ the same. When you use is on class references, it's true if and only if the two references point to the same object. It's basically doing a pointer comparison. In contrast, == calls the free function, opEquals, which will call opEquals on the class references to compare them if they're not null and they're not pointing to the same object.
Right. I was just saying that `is` will in most cases give the same results as the default `opEquals` function. The existence of `is` means I no longer think `opEquals` should be ` safe` by default. The only place where they would differ is when the first reference is null, right? Ultimately, not knowing to use `is` is what lead me to making this topic. Actually, I just looked in [the base object definition](https://github.com/dlang/dmd/blob/master/druntime/src/object.d), and I found this: ``` bool opEquals(Object o) { return this is o; } ``` It looks like it really would be the same whenever the first reference is a valid object. I figure it would segfault if the first reference is null, while `is` would simply return `false`.
 You already need to do an override to get opEquals unless you 
 want it to just compare the references, which is borderline 
 useless. All that opEquals on Object does is compare the 
 addresses of the references with the is operator, and that's 
 almost never what you want when doing an equality check.
Well, it's exactly what I wanted, but I'm new to the language, and hadn't yet been taught to use `is` in such cases. For the function I was writing, see `Weapon.getOptions` or look for `if (user.currentWeapon == this)` in [this module](https://github.com/LiamM32/Open_Emblem/blob/master/source/item.d). Now knowing about the proper uses of `==` and `is`, I think the only worthy idea I have for a new addition to the language is a new function in `std.algorithm.searching`; a ` safe bool` function called `contains`. It would return whether the first argument (an array of the second argument's type) contains the second argument. It would be equivalent to `canFind` for some argument types, but for objects it would do an `is` comparison. Perhaps I can try to implement this myself. Given my inexperience, chances are someone else would make some edits to it before getting merged into Phobos.
Apr 07
parent Nick Treleaven <nick geany.org> writes:
On Sunday, 7 April 2024 at 21:05:14 UTC, Liam McGillivray wrote:
 default. The only place where they would differ is when the 
 first reference is null, right?
No. Maybe you missed Jonathan's example?: auto a = new C(42); auto b = new C(42); then assert(a is b); would fail, whereas assert(a == b); would pass.
Apr 08
prev sibling next sibling parent reply "H. S. Teoh" <hsteoh qfbox.info> writes:
On Thu, Apr 04, 2024 at 11:56:05PM -0600, Jonathan M Davis via Digitalmars-d
wrote:
[...]
 Ideally, Object wouldn't have functions like opEquals, and they'd only
 be added by derived classes so that programs could put whatever
 attributes on them make the most sense, but that's a breaking change,
 so it hasn't happened.
[...] We've been talking about ProtoObject for years now. When will that materialize? T -- There is no gravity. The earth sucks.
Apr 05
next sibling parent "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
On 06/04/2024 5:02 AM, H. S. Teoh wrote:
 On Thu, Apr 04, 2024 at 11:56:05PM -0600, Jonathan M Davis via Digitalmars-d
wrote:
 [...]
 Ideally, Object wouldn't have functions like opEquals, and they'd only
 be added by derived classes so that programs could put whatever
 attributes on them make the most sense, but that's a breaking change,
 so it hasn't happened.
[...] We've been talking about ProtoObject for years now. When will that materialize? T
Custom ``extern(D)`` class roots are on my list which could be a better solution. But yeah somebody needs to write the DIP. D classes are just one more thing that ties the language to druntime, and I really want to see that relationship broken up.
Apr 05
prev sibling parent Alexandru Ermicioi <alexandru.ermicioi gmail.com> writes:
On Friday, 5 April 2024 at 16:02:27 UTC, H. S. Teoh wrote:
 We've been talking about ProtoObject for years now. When will 
 that materialize?
Why do even need proto object? Can't we just have a bunch of interfaces for each kind of operator and attribute set? For example: ```d interface Equatable(Attrs...) { bool opEquals(Equatable!Attrs other); } // and etc. ``` Those interfaces could perfectly work as object roots, when required, and no need for having an actual class as root for all objects. Best regards, Alexandru.
Apr 07
prev sibling parent Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
On Friday, April 5, 2024 10:02:27 AM MDT H. S. Teoh via Digitalmars-d wrote:
 On Thu, Apr 04, 2024 at 11:56:05PM -0600, Jonathan M Davis via Digitalmars-d
 wrote: [...]

 Ideally, Object wouldn't have functions like opEquals, and they'd only
 be added by derived classes so that programs could put whatever
 attributes on them make the most sense, but that's a breaking change,
 so it hasn't happened.
[...] We've been talking about ProtoObject for years now. When will that materialize?
Ideas have been discussed, but no one ever implemented anything, and I don't know what the exact issues are with it at this point other than the simple fact that it requires that someone do the work. At this point, we'd need a DIP and someone to actually go and implement it, and it's not something that anyone has chosen to take the time to tackle. It may end up being easier with Editions, though since we need to support multiple Editions with the same druntime, there are some things that we'll never be able to do (e.g. we're pretty sure that we can't ever remove the monitor from Object, because then the previous Editions would all break due to binary incompatibilities, though if we have a ProtoObject, then we can hopefully have a separate hierarchy without the monitor). So, as is often the case, I think that it's a combination of problems related to manpower and priorities. - Jonathan M Davis
Apr 05