Thread overview
April 05

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?

April 04
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



April 05
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.
April 06
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.
April 05
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



April 07

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.

April 07
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



April 07

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:

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.

April 07

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

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.

April 08

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.