Destructors, const structs, and opEquals

Don nospam at nospam.com
Sat Dec 4 12:39:10 PST 2010


Andrei Alexandrescu wrote:
> On 12/4/10 9:23 AM, Don wrote:
>> Andrei Alexandrescu wrote:
>>> On 12/4/10 12:42 AM, Don wrote:
>>>> Officially, opEquals has to have the signature:
>>>>
>>>> struct Foo {
>>>> bool opEquals(const ref Foo x) const {...}
>>>> }
>>>
>>> This is a compiler bug. For structs there should be no official
>>> implementation of opEquals, opCmp etc. All the compiler needs to worry
>>> about is to syntactically translate a == b to a.opEquals(b) and then
>>> let the usual language rules resolve the call.
>>
>> Fine, but try to implement a generic type which supports ==.
>> For example, Tuple in std.typecons.
>> The only reason that many of the Tuple unit tests pass is that opEquals
>> never gets instantiated.
> 
> I think it should be as follows:
> 
> bool opEquals(auto ref inout Tuple rhs) inout {
>   foreach (i, T; Types) {
>     if (this[i] != rhs[i]) return false;
>   }
>   return true;
> }
> 
> It looks a bit alembicated but let's not forget that Tuple is supposed 
> to be very flexible and to do a lot of things.

Ouch.

The semantics of == are very well defined, and simple. Always, you want 
read-only access to the two objects, in the fastest possible way.
I don't see why the complexity of the object should have any influence 
on the signature of ==. If there's a method which works correctly and 
efficiently in every case, why isn't it the only way?


>> How can opEquals be defined in a way that it works for structs with
>> destructors, and also with rvalues?
> 
> "auto ref" should be used whenever you want to accept both a value and 
> an rvalue. For structs with destructors see my other post in this thread.
> 
> Unfortunately, "auto ref" is currently implemented wrongly due to a 
> misunderstanding between Walter and myself. I meant it as a relaxation 
> of binding rules, i.e. "I'm fine with either an rvalue or an lvalue". He 
> thought it's all about generating two template instantiations. In fact 
> auto ref should work when there's no template in sight.
> 
>>>> But this disallows comparisons with rvalues.
>>>> eg,
>>>>
>>>> Foo bar() { Foo x = 1; return x; }
>>>> Foo y=1;
>>>> assert( y == bar() ); // doesn't compile
>>>>
>>>> You can get around this by declaring a non-ref opEquals.
>>>> But this fails if Foo has a destructor.
>>>>
>>>> If a struct has a destructor, it cannot be const(this is bug 3606)
>>>> ---
>>>> struct S {
>>>> ~this() {}
>>>> }
>>>>
>>>> void main() {
>>>> const S z;
>>>> }
>>>> ---
>>>> bug.d(6): Error: destructor bug.S.~this () is not callable using
>>>> argument types ()
>>>> -------
>>>> Likewise, it can't be a const parameter (this is bug 4338).
>>>> void foo(const S a) {}
>>>> It works to have it as a const ref parameter.
>>>>
>>>> Everything will work if you declare a const ~this(), but that seems a
>>>> little nonsensical. And you cannot have both const and non-const
>>>> ~this().
>>>>
>>>> I'm a bit uncertain as to how this is all supposed to work.
>>>> (1) Should temporaries be allowed to be passed as 'const ref'?
>>>> (2) If a struct has a destructor, should it be passable as a const
>>>> parameter? And if so, should the destructor be called?
>>>
>>> This is a delicate matter that clearly needs a solution. Pass of
>>> temporaries by const ref was a huge mistake of C++ that it has paid
>>> dearly for and required the introduction of a large complication, the
>>> rvalue references feature, to just undo the effects of that mistake.
>>> So I don't think we should allow that.
>>>
>>> Regarding destructors, for every constructed object ever there must be
>>> a corresponding destructor call. One issue that has been a matter of
>>> debate in C++ has been the fact that any object becomes "deconstified"
>>> during destruction. The oddest consequence of that rule is that in C++
>>> you can delete a pointer to a const object:
>>>
>>> // C++ code
>>> class A { ... };
>>> void fun(const A* p) { delete p; /* fine */ }
>>>
>>> There has been a lot of opposition. const is supposed to limit what
>>> you can do with that object, and the fact that you can't invoke
>>> certain methods or change members, but you can nuke the entire object,
>>> is quite nonintuitive (and leads to a lot of funny real-life
>>> comparisons such as "You can go out with my daughter, but no touching.
>>> Of course, you can shoot her if you so wish.")
>>>
>>> In D, the rule must be inferred from D's immutability rules, which
>>> pretty much dictate that the destructor must be overloaded for
>>> non-const and const (and possibly invariant if the struct needs that).
>>
>> This scares me. I can see a danger of making structs with destructors
>> practically unusable.
> 
> Why? It makes perfect sense to qualify the destructor the same way as 
> the originating constructor. It has been a serious limitation of C++ 
> that you couldn't tell during either construction or destruction that a 
> const object was being built/destroyed.
> 
> One issue with D's const is that people expect to use it most 
> everywhere, much like C++'s const. One thing that I understood early on 
> was that D's const provides much stronger guarantees than C++'s, and as 
> a direct consequence it is more constrained and is usable less often.
> 
>> Some unprocessed thoughts are below.
>>
>> Who calls postblit? I think that if you can make a clone of a const
>> object, you should also be able to destroy that clone.
> 
> Yes. Any object created will also be destroyed, regardless of qualifiers.
> 
>> Seems to me that the natural rule would be, that the creator is
>> responsible for destruction.
> 
> That rule is roughly C++'s and has an issue that D fixes (in my mind; 
> the implementation is not 100% there yet). Exact issue is discussed below.
> 
>> This would require that, for example, given
>> code like this:
>>
>> const(S) foo(const(S) x) { return x; }
>>
>> inside foo, a non-const S is created, blitted with x, then postblit is
>> called on it, then it is returned as const. On return, the original x is
>> destroyed if it was a temporary. Otherwise, it gets called at the end of
>> its scope.
>>
>> Eg,
>>
>> const w = foo(foo( S(2) ));
>>
>> would be translated into:
>> const w = (foo( S __tmp1 = S(2), const(S) __tmp2 = foo(__tmp1), ~__tmp1,
>> __tmp2), ~__tmp2);
>>
>>
>> Has this sort of idea been explored? Is there something wrong with it?
> 
> What's wrong with it is it consistently leads to suboptimal code, which 
> has created a hecatomb of problems for C++ (even the very carefully 
> conceived rvalue references feature, for all its size and might, is 
> unable to fix them all).
> 
> The caller should create the copy and pass the responsibility of 
> destroying to the callee. This is because oftentimes the actual object 
> destroyed is not the same object that was constructed. You see those 
> trailing calls ~__tmp1, ~__tmp2 at the end of your code? They are a tin 
> cat stuck to code's tail.

They need not apply to functions with 'inout' parameters. A parameter 
which is passed by 'inout' will either be used in the return value, or 
it will need to be destroyed.
It seems clear to me that when you declare an 'inout' parameter, you're 
  assuming responsibility for the lifetime of the object.


More information about the Digitalmars-d mailing list