Delete - possibility to define behaviour?

160 views
Skip to first unread message

willi...@gmail.com

unread,
Feb 26, 2014, 6:13:02 PM2/26/14
to std-pr...@isocpp.org
It's common to derive from a class and only add things like convenience constructors. That means the memory footprint (and object size) of the base class and the derived class is exactly the same and there's no risk of dangerous slicing. However, in case the base class lacks a virtual destructor, it's a bit scary that there's nothing that will prevent accidental deletion through a pointer-to-base, which causes undefined behaviour.

I'm playing with the thought: would it be possible to define the behaviour of a delete operation via pointer-to-base in this particular case, in a future version of the standard?

As far as I can see, there are two real dangers when deleting via pointer-to-base in general when there's no virtual destructor. The first one lies in invoking delete with the wrong object size (derived class members are not properly destroyed). That will always cause undefined behaviour, but in the case described above when the size is the same (and there are no derived class members to destroy) it should be safe.

The other one is that the destructor of the derived class doesn't get called, but only the base class destructor. This is of course very bad, but in theory (or at least in my mind) calling the "wrong" destructor has the potential of not causing undefined behaviour. Let's just pretend that the behaviour is defined such that the base non-virtual destructor is called and any destructors of derived objects are not. When there are no derived members to destroy, this may "only" lead to unexpected results (resources not being released, or whatever the omitted destructor was supposed to do unless it's empty) in case of accidential deletion through a pointer-to-base. But at least the behaviour would be defined and reproducable with different compilers! I think that is a slightly better (less evil) alternative to undefined behaviour, if there's any chance of possible standardization.

Of course, this still does not mean that anyone should intentionally write code that deletes via base-to-pointer!

Please share your reactions, or tell me if I'm wrong somewhere. Maybe there are other negative effects in "compiler magic" that I'm not aware of?

Best wishes

Johan

Ville Voutilainen

unread,
Feb 26, 2014, 6:26:58 PM2/26/14
to std-pr...@isocpp.org
It seems that this idea was floated on
http://www.open-std.org/pipermail/ub/2013-December/000382.html
and the replies to it.

Ville Voutilainen

unread,
Feb 26, 2014, 6:36:52 PM2/26/14
to std-pr...@isocpp.org
On 27 February 2014 01:13, <willi...@gmail.com> wrote:
>
> The other one is that the destructor of the derived class doesn't get
> called, but only the base class destructor. This is of course very bad, but
> in theory (or at least in my mind) calling the "wrong" destructor has the
> potential of not causing undefined behaviour. Let's just pretend that the
> behaviour is defined such that the base non-virtual destructor is called and
> any destructors of derived objects are not. When there are no derived
> members to destroy, this may "only" lead to unexpected results (resources
> not being released, or whatever the omitted destructor was supposed to do
> unless it's empty) in case of accidential deletion through a
> pointer-to-base. But at least the behaviour would be defined and
> reproducable with different compilers! I think that is a slightly better
> (less evil) alternative to undefined behaviour, if there's any chance of
> possible standardization.


Another point; having such mistakes be defined behavior is not necessarily
"less evil". Undefined behavior doesn't mean "WILL launch missiles", it
also means that an implementation is allowed to do _useful_ things under
the auspices of undefined behavior, like launch a debugger and tell you
where you made a mistake!

Given that, and the email thread on the UB mailing list (and especially
the first response to the initial question), I think you need a
stronger argument
for making the case you depict defined. It seems to me that it should remain
undefined, because _that_ is the "less evil" alternative.

willi...@gmail.com

unread,
Feb 27, 2014, 4:01:49 AM2/27/14
to std-pr...@isocpp.org, willi...@gmail.com

Ville, I see your point, but so far I still think defined behaviour would be preferred.

The only case when you will have a bug and defined behaviour at the same time, is when the desctructor is non-empty. Of course, this kind of bugs are hard to track down. But if the destructor is supposed to do some work, and for the reason of delete via pointer-to-base is not executed, the program will not run as you expected it to. There can still be symptoms. Most importantly, the program remains in a valid state. Your system tests will still work in production.

The undefined behaviour on the other hand is unpredictable, so anything can happen. A program that can't be trusted may be useless or dangerous at worst. If you're lucky, that missile you mention will visibly go off in debug mode before you go into production. Let's pray the missile dives into the ocean. If you're not lucky, some seemingly random minor change will happen that keeps your program running without any diagnostics being reported, and you will never know why your program state changed.

You've got to ask yourself one question: "Do I feel lucky?" :-) Seriously though, I will try to come up with some more arguments.
 
Kind regards,
 
Johan
 
 

Ville Voutilainen

unread,
Feb 27, 2014, 4:10:52 AM2/27/14
to std-pr...@isocpp.org
On 27 February 2014 11:01, <willi...@gmail.com> wrote:
> Ville, I see your point, but so far I still think defined behaviour would be
> preferred.
>
> The only case when you will have a bug and defined behaviour at the same
> time, is when the desctructor is non-empty. Of course, this kind of bugs are
> hard to track down. But if the destructor is supposed to do some work, and
> for the reason of delete via pointer-to-base is not executed, the program
> will not run as you expected it to. There can still be symptoms. Most
> importantly, the program remains in a valid state. Your system tests will
> still work in production.

Well, as far as I can see, the program is most certainly not in a
valid state, nor will
system tests work in production. I have no way of assuming that not invoking
the correct destructor results in a valid state, so I'll assume quite
the opposite.
And the tests I tend to write will show whether the right destructor was invoked
or not.


> The undefined behaviour on the other hand is unpredictable, so anything can
> happen. A program that can't be trusted may be useless or dangerous at
> worst. If you're lucky, that missile you mention will visibly go off in
> debug mode before you go into production. Let's pray the missile dives into
> the ocean. If you're not lucky, some seemingly random minor change will
> happen that keeps your program running without any diagnostics being
> reported, and you will never know why your program state changed.

The same applies equally to a program that didn't invoke a destructor
it was meant to.

I can probably catch such UB with ub-sanitizer. If the behavior becomes defined,
I don't know how any diagnostic tool will know whether I did or did not mean
to not invoke a destructor.

I remain strongly convinced that the current semantics are right.

willi...@gmail.com

unread,
Feb 27, 2014, 4:22:21 AM2/27/14
to std-pr...@isocpp.org, willi...@gmail.com
>Well, as far as I can see, the program is most certainly not in a valid state, nor will
>system tests work in production. I have no way of assuming that not invoking
>the correct destructor results in a valid state, so I'll assume quite
>the opposite.
 
That's the main point. I'm asking if it would be possible to define the behaviour so that the outcome is predictable and reproducable. That's what I meant with valid state.

Best wishes

Johan

David Krauss

unread,
Feb 27, 2014, 4:53:04 AM2/27/14
to std-pr...@isocpp.org
I think the answer is that the ideas expressed by the C++ type system don’t permit that possibility. Two objects constructed with different types have different type throughout their lifetime, and each must be destroyed by the appropriate destructor. The type system isn’t as easy to hack as you’re assuming.

See the discussion at the ub list. The problem solved by pseudo-subtyping touches on extension methods and factory functions. Improving those areas of the language (and they need improvement) should eliminate the desire to attempt to emulate a base class up to destructor compatibility.

Ville Voutilainen

unread,
Feb 27, 2014, 6:19:07 AM2/27/14
to std-pr...@isocpp.org
Well, I guess it's certainly possible, for cases where the derived
class destructor
is trivial. That would still be validating a mostly-invalid design in
language rules, and I
can't imagine a practical reason to do so. Having it be UB allows
implementations
to detect the situation (at run-time, if need be) and inform users,
without forcing every
implementation to do so (thus avoiding potential run-time costs that
don't need to
be paid unless so desired). This also nicely allows implementations to have the
detection mechanism in place in debug mode, and to remove such mechanisms
in release mode.

willi...@gmail.com

unread,
Feb 27, 2014, 7:20:03 AM2/27/14
to std-pr...@isocpp.org, willi...@gmail.com
Ville, are you referring to Clang's UB sanitizer? Is this situation really so easily detectable and this tool can tell where you made the mistake? That is not my personal experience from the tools I'm using, but I guess I should test with some other tools as well.
 
However, it's my understanding that undefined behaviour does not necessarily manifest itself in any measurable way. Or it may show up the first time your run the program on a different operating system version, hardware or some other circumstances. Or after several weeks of running the software continously. It doesn't sound very easy to detect in a debug session.
 
Best wishes
 
Johan

David Krauss

unread,
Feb 27, 2014, 8:25:33 AM2/27/14
to std-pr...@isocpp.org

On Feb 27, 2014, at 7:19 PM, Ville Voutilainen <ville.vo...@gmail.com> wrote:

> Well, I guess it's certainly possible, for cases where the derived
> class destructor
> is trivial.

The lifetime of trivially-destructible types ends arbitrarily; it doesn’t matter whether the destructor runs or not. I don’t think the current language specifies UB if you happen to arbitrarily call the trivial destructor of a base subobject before neglecting to call the trivial destructor of the derived class.

Perhaps you’re even allowed to call the same trivial destructor twice. I don’t see why not; they don’t do anything at all. (ATM I had a couple drinks and it’s bedtime though.)

Ville Voutilainen

unread,
Feb 27, 2014, 8:31:55 AM2/27/14
to std-pr...@isocpp.org
The current wording doesn't care whether the destructors are trivial.
The case we hit
is [expr.delete]/3, which says
"if the static type of the object to be deleted is different from its
dynamic type, the static type shall be a base class of the dynamic
type of the object to be deleted and the
static type shall have a virtual destructor or the behavior is undefined. "

An alternative case is [expr.delete]/5, which says "If the object
being deleted has incomplete
class type at the point of deletion and the complete class has a
non-trivial destructor or a deallocation function, the behavior is undefined."

That doesn't supersede paragraph 3, as far as I can see. So, as long
as the static and
dynamic type differ, deleting is undefined behavior if the destructor
is not virtual. Therefore,
it doesn't help that either the base or derived destructor is trivial.
That's what this thread
is originally proposing to change.

Ville Voutilainen

unread,
Feb 27, 2014, 8:36:50 AM2/27/14
to std-pr...@isocpp.org
On 27 February 2014 14:20, <willi...@gmail.com> wrote:
> Ville, are you referring to Clang's UB sanitizer? Is this situation really
> so easily detectable and this tool can tell where you made the mistake? That
> is not my personal experience from the tools I'm using, but I guess I should
> test with some other tools as well.

I do not know whether that particular sanitizer (which is not
clang-specific, I might
add) supports this particular case. But I do not expect adding such
support to be
too difficult.

>
> However, it's my understanding that undefined behaviour does not necessarily
> manifest itself in any measurable way. Or it may show up the first time your
> run the program on a different operating system version, hardware or some
> other circumstances. Or after several weeks of running the software
> continously. It doesn't sound very easy to detect in a debug session.

All of these problems apply equally well to the cases where you don't invoke
a destructor you intended to invoke. Whether an implementation gives you
sane results for such UB cases is up to the implementation. Whether making
the case this thread is about ends up having sane results is up to the user
code. I prefer giving an implementation a chance to mark all of these cases
invalid, rather than relying on the users to instrument what they need
and figure
out which valid cases are actually valid and which ones are actually invalid.

Just in case it's not clear, based on what I have written on this
subject, you can
expect national body opposition to this proposal.

David Rodríguez Ibeas

unread,
Feb 27, 2014, 10:52:01 AM2/27/14
to std-pr...@isocpp.org
I believe that the original question is not addressed only to types with trivial destructors as defined in the standard, but rather to types for which the dynamic type's destructor does not do anything other than calling the static type's destructor. Something like:

struct base {
   std::string s;
};
struct derived : base {
};
delete static_cast<base*>(new derived);

The destructor is not trivial, as the base's destructor is not trivial, but Johan believes that since '~derived' does not do anything other than calling '~base' and that one will be called in that 'delete' expression.

Other than that, I am not sure I buy into that use case. If the type does not have a virtual destructor, it was most probably not designed to be a base of anything else, and in that case you might be abusing inheritance. If you only want convenience constructors (as you mentioned), you can always do that externally by creating functions that will create the object and set it to the correct state. The design looks suspicious...



--

---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
Visit this group at http://groups.google.com/a/isocpp.org/group/std-proposals/.

Nevin Liber

unread,
Feb 27, 2014, 11:10:05 AM2/27/14
to std-pr...@isocpp.org
On 26 February 2014 17:13, <willi...@gmail.com> wrote:
It's common to derive from a class and only add things like convenience constructors. That means the memory footprint (and object size) of the base class and the derived class is exactly the same and there's no risk of dangerous slicing.

Memory footprint: yes.  Behavior: no.

When there are no derived members to destroy, this may "only" lead to unexpected results (resources not being released, or whatever the omitted destructor was supposed to do unless it's empty) in case of accidential deletion through a pointer-to-base.

A derived destructor may modify global/static/thread local state or base class state.

About the only possible rule you could use safely is if you have a derived class that doesn't add any non-trivially destructible non-static member variables nor a non-defaulted user declared destructor.

That sounds like a very complicated rule leading to very brittle code.

But at least the behaviour would be defined and reproducable with different compilers! I think that is a slightly better (less evil) alternative to undefined behaviour, if there's any chance of possible standardization.

Of course, this still does not mean that anyone should intentionally write code that deletes via base-to-pointer!

The only way the standard can say "don't do that" is by making it undefined behavior.  If the code is legal, users are allowed to rely on that behavior.

I would be strongly against this, were it to be a proposal.
--
 Nevin ":-)" Liber  <mailto:ne...@eviloverlord.com(847) 691-1404

Thiago Macieira

unread,
Feb 27, 2014, 12:39:32 PM2/27/14
to std-pr...@isocpp.org
Em qui 27 fev 2014, às 01:01:49, willi...@gmail.com escreveu:
> The only case when you will have a bug and defined behaviour at the same
> time, is when the desctructor is non-empty. Of course, this kind of bugs
> are hard to track down. But if the destructor is supposed to do some work,
> and for the reason of delete via pointer-to-base is not executed, the
> program will not run as you expected it to. There can still be symptoms.
> Most importantly, the program remains in a valid state. Your system tests
> will still work in production.

Says who? You just said that some code that was supposed to be run didn't get
run. I call that "invalid state".

Example to prove: the derived class's destructor removes the this pointer from
a global list of tracked objects, which was previously added by the
constructor. This now means the global list contains a dangling pointer.

It sounds like you want "it's defined behaviour if the derived class(es)
destructor(s) are trivial, if you don't count the call to the base class
destructor"
--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center
PGP/GPG: 0x6EF45358; fingerprint:
E067 918B B660 DBD1 105C 966C 33F5 F005 6EF4 5358

David Krauss

unread,
Feb 27, 2014, 9:14:53 PM2/27/14
to std-pr...@isocpp.org
On Feb 27, 2014, at 9:31 PM, Ville Voutilainen <ville.vo...@gmail.com> wrote:

On 27 February 2014 15:25, David Krauss <pot...@gmail.com> wrote:

On Feb 27, 2014, at 7:19 PM, Ville Voutilainen <ville.vo...@gmail.com> wrote:

Well, I guess it's certainly possible, for cases where the derived
class destructor
is trivial.

The lifetime of trivially-destructible types ends arbitrarily; it doesn't matter whether the destructor runs or not. I don't think the current language specifies UB if you happen to arbitrarily call the trivial destructor of a base subobject before neglecting to call the trivial destructor of the derived class.

Perhaps you're even allowed to call the same trivial destructor twice. I don't see why not; they don't do anything at all. (ATM I had a couple drinks and it's bedtime though.)

The current wording doesn't care whether the destructors are trivial.
The case we hit
is  [expr.delete]/3, which says
"if the static type of the object to be deleted is different from its
dynamic type, the static type shall be a base class of the dynamic
type of the object to be deleted and the
static type shall have a virtual destructor or the behavior is undefined. “

Ah, didn’t realize that dynamic type applies to non-polymorphic classes.

There’s a slight ambiguity in that paragraph, though. If you consider subobjects to be “created by new” along with the complete object, then pointers to member subobjects (which are most-derived objects) are valid arguments to delete. References to most-derived object in [expr.delete]/2 should be replaced with complete object.

That doesn't supersede paragraph 3, as far as I can see. So, as long
as the static and
dynamic type differ, deleting is undefined behavior if the destructor
is not virtual. Therefore,
it doesn't help that either the base or derived destructor is trivial.
That's what this thread
is originally proposing to change.

I get the impression he wants to work with real destructors.

Trivial destructors might not be interchangeable with delete, but free still remains a viable solution as long as the base class overloads operator new to call malloc. (Or otherwise ensure that malloc is called.)

Reply all
Reply to author
Forward
0 new messages