Object.toString, toHash, opCmp, opEquals

Jonathan M Davis newsgroup.d at jmdavisprog.com
Fri Apr 26 08:43:46 UTC 2024


On Friday, April 26, 2024 12:44:03 AM MDT Walter Bright via Digitalmars-d 
wrote:
> Perhaps I can help things work for you and Timon:
>
> ```
> import std.stdio;
>
> class A
> {
>      string xxx(const Object) const { return "A"; }
> }
>
> class B : A
> {
>      alias xxx = A.xxx;
>      string xxx(Object) { return "B"; }
> }
>
> void main()
> {
>      const A a = new A();
>      B b = new B();
>      const B c = new B();
>      writeln(a.xxx(a));
>      writeln(b.xxx(b));
>      writeln(c.xxx(c));
> }
> ```
> I'm calling this xxx instead of toString, just so I can show all the code.
> Compiling it and running it prints:
>
> A
> B
> A
>
> In other words, you can have a toString() that is mutable and it will work
> fine with writeln(), because writeln(x) does not look for
> Object.toString(), it looks for x.toString().
>
> Does this work for you?

The ideal situation here is that none of these functions are on Object at
all. They really aren't useful there, because it's not particularly useful
or necessary to operate on Object. Some of the druntime code does, because
it hasn't been templated yet, but once it has been, it won't need to operate
on Object at all. At that point, we won't need to have any of these
functions on Object, and Editions should give us the ability to remove them.

And then, yes, classes can define those functions as they see fit without
having to worry about an implementation on Object, because the code that's
using them will be templated. To an extent, that can be done now. However,
you then have a problem if the Object version ever gets called, and the
derived version does not override the Object version, because then the wrong
version gets called. And if we add something like const to these functions,
and Object gets used at all, then either the wrong overload will be called,
or the derived class will need to be casting away const to have the correct
version called (or druntime will be casting away const like it unfortunately
does with opEquals right now if you compare two const Objects, and when that
happens, it can easily violate the type system's guarantees with const).

Adding const to any of these functions on Object is just putting the code in
a position where either we're at risk of the wrong overload being called, or
const is going to end up being cast away if the Object overload ever gets
used. And it really doesn't buy us much, since code that wants to have them
work with const can overload them on const right now and have the non-const
overload call the const one. Object can't be compared as const, but that's
not generally necessary anyway, since normal code is going to use reference
which are typed as the actual classes, not Object. For the most part, the
only code that's going to have that issue is the druntime code that we need
to turn into templates anyway but currently uses Object because of how old
it is. And once that's done, then there's no need to have the Object
versions of these functions at all. So, I don't think that it makes any
sense whatsoever to add const to the functions on Object. Rather, we need to
be getting the druntime code to the point that it will work to remove them
from Object. And that will fix far more than the issue of const working with
these functions, because then it will allow user code to define these
functions with whatever set of attributes make sense for that code, thereby
fixing the problem for attributes in general.

Also, I would point out that if your motivation for trying to put const on
these functions is related to DIP 1021, then that's going to cause a whole
other set of problems anyway, because if I understand correctly, DIP 1021 is
trying to disallow stuff like

    foo(bar, bar);

where bar is not taken as const via at least one of those parameters. And
yet it's extremely common that neither parameter can be marked as const,
because the code is either written to work with a type that does not work
with const, or it's templated and therefore needs to not assume that the
type it's given works with const (and will often not be instantiated with
const types). The free function, opEquals, is precisely such a case, becase
it's not only designed to work with classes whether their opEquals is const
or not (so long as they're not compared as Object), but it's specifically
designed to be able to compare the same class object against itself (whether
it's literally the same reference or two references which happen to point to
the same object). So, the free function, opEquals, needs to be able to
accept the same object for both arguments without using const at all. Stuff
like

    auto eq = cls == cls;

or

    auto cls2 = cls;
    auto eq = cls == cls2;

need to compile (especially the second one), and that needs to work without
requiring const, because not all classes can use const.

And if DIP 1021 is trying to force code to use const, that's going to be
non-starter for a lot of code because of how restrictive D's const is. How
common it is for the same reference to be passed multiple times, I don't
know (certainly, it's going to be far more common with opEquals or opCmp
than with most functions), so the issue may be fairly restricted in
practice, but with pretty much any part of D, it's going to be a problem any
time that the language tries to require that stuff be const, because const
is simply too restrictive to work with code in general. It would be like
requiring that a feature be pure or @nogc. It will work in many cases, but
it also won't work in many cases, so requiring it makes it so that code that
really should work won't.

Of course, const will work with some types just fine (especially primitive
types), but there are lots of cases in D code where const is avoided
completely, because it's too restrictive for that code to use. And templated
code typically avoids explicitly using it at all on its parameters, because
if the parameters were explicitly marked as const, the code wouldn't work
with any types that don't work as const, whereas if you don't mark them as
const and then pass a const object, then the template is instantiated with
const and works just fine so long as the type itself works as const. So, for
most templated code, explicitly using const is not only completely
unnecessary, but it's bad practice.

So, I find it to be extremely concerning if we're trying to force const
anywhere. We need to support it where we can, but requiring it is going to
cause problems with any types that can't use it - or which can't use it for
the particular operations that are involved with the code trying to require
it. So, if a new language feature is trying to require const, we really need
to revisit that feature.

- Jonathan M Davis





More information about the Digitalmars-d mailing list