Object.toString, toHash, opCmp, opEquals

Jonathan M Davis newsgroup.d at jmdavisprog.com
Fri Apr 26 02:16:05 UTC 2024


On Thursday, April 25, 2024 6:57:49 PM MDT Walter Bright via Digitalmars-d 
wrote:
> On 4/25/2024 4:36 PM, Timon Gehr wrote:
> >> Without the `const` annotations, the functions are not usable by `const`
> >> objects without doing an unsafe cast. This impairs anyone wanting to
> >> write
> >> const-correct code,
> >
> > "const correctness" does not work in D because const
> >
> > a) provides actual guarantees
> > b) is transitive
>
> It's not the C++ notion of const, sure. But the name still applies.

The name applies, but because D's const is transisitive and can't be
backdoored, it poses a serious problem for certain categories of types to
require it. As such, while in C++, it's normal to slap const on stuff all
over the place, because the type is logically const, and any type that needs
to mutate any portion of its state which is not part of that logical
constness (e.g. a mutex) is perfectly free to do so via using the mutable
keyword or casting away const. In contrast, it violates the type system for
any D code to work around const like that, so it becomes problematic to use
const all over the place like you would in C++, and making code "const
correct" like you would in C++ is typically bad practice in D. It's great to
use D's const where you can, but it's simply too restrictive to require it
in the general case.

> > It is fundamentally incompatible with many common patters of
> > object-oriented and other state abstraction. It is not even compatible
> > with the range API. Uses of `const` are niche. `const` is nice when it
> > does work, but it's not something you can impose on all code,
> > particularly object-oriented code.
>
> Why would anyone, for example, try to mutate a range when it is passed to
> one of these functions?

If you can't mutate a range, you can't iterate through it. Your proposed DIP
to be able to have a form of tail-const for ranges will help with that, but
the fact still stands that some types will not work with const, because they
need to mutate some portion of their state in order to function, even with
functions that need to be logically const. If D's const were like C++'s
const, this wouldn't be a problem, but the strong guarantees that D's const
is supposed to provide make it completely incompatible with some code. As
such, we really can't require it anywhere without causing problems. If you
want to be able to require it, it needs to have backdoors; otherwise, a
number of common coding idioms become impossible to use.

So, either we have backdoors that allow mutating const, and we can require
const in places that need to be logically const, or we have const be strict
about mutation and can't require that it be used. As things stand with D's
const, that means that we can't require that it be used.

> >> I recommend that everyone who has overloads of these functions, alter
> >> them to have the `const` signatures. This will future-proof them against
> >> any changes to Object's signatures.
> >
> > I will not do that, because if it does not outright break my code (e.g.
> > because Phobos cannot support `const` ranges), it actually limits my
> > options in the future in a way that is entirely unnecessary.
>
> Why would anyone need toHash(), toString(), opEquals() or opCmp() to mutate
> their data? Wouldn't that be quite surprising behavior?

It would be surprising if the logical state of the type changed, but it
wouldn't be at all surprising if some portion of the type which was not part
of its logical state changed. A very simple case of this would be if the
type contains a member variable which is shared and a mutex to protect
access to that data (be it a mutex which is also a member variable or which
is a member of the shared member variable). Any of those four functions
would then need to lock that mutex in order to read the data so that they
can do stuff like hash it or compare it. So, while the logical state
wouldn't change, the object itself would be mutated in the process.

Similarly, if a type lazily initializes some portion of its state, and that
initialization hasn't happened yet before one of those functions is called,
then it's going to have to do that initialization as part of the call, which
means mutating the object's state. Its logical state doesn't change, so for
C++, this kind of thing would be a complete non-issue, but for D, because
const doesn't allow any kind of mutation, such a type cannot have const
functions.

And those are just two examples of cases where an object needs to be able to
mutate some portion of its state in functions like opEquals, meaning that if
we put const on opEquals, either such classes can no longer be written in D,
or they're going to cast away const and mutate even if that does technically
violate the type system's guarantees.

If you're just dealing with ints and pointers and arrays and the like, and
you aren't dealing with user-defined types at all, then const generally
doesn't cause many problems. But as soon as you're dealing with user-defined
types, you start running into issues with const depending on what your code
needs to do, and the more complex the code, the more likely it is that
issues with const are going to pop up. The same goes with pretty much all of
the attributes. They add restrictions which work in some cases but don't in
many others.

So, for instance, it's usually bad practice to put const on the parameters
for templated functions, since that means that whole categories of types
won't work with that code, whereas if you don't use const, the caller can
pass a const type, and it'll work just fine in that case so long as the type
in question was designed to work with const. But the types that don't work
with const will also work with that code, because the template doesn't have
its parameter marked as const, and so the generated code won't use const.

We have the same problem with member functions on classes, but since they're
virtual, we can't templatize that code. However, we can templatize the code
that uses those classes, making the use of Object completely unnecessary,
and then each class can define functions like opEquals with whatever set of
attributes makes sense for that class' hierarchy. Derived classes within
that hierarchy will then be stuck with the decisions made for the base
class, but programmers can choose what makes the most sense for that
particular class hierarchy, whereas we cannot possibly make that decision
for all classes and not screw over developers in the process, because it's
not one size fits all.

In general, we need to be trying to support the various attributes
(including const) with druntime and Phobos, but we should not be requiring
them, because they are all too restrictive for that to make sense. And that
includes const.

- Jonathan M Davis





More information about the Digitalmars-d mailing list