Discussion:
Differentiating destructor invocation contexts
Jens Maurer
2012-12-10 16:32:40 UTC
Permalink
Here's a proposal to offer a core feature where std::uncaught_exception() fails.
Also available at http://jmaurer.awardspace.info/wg21/destructor-unwinding.html .

Feedback is welcome.

Jens

--
Vicente J. Botet Escriba
2012-12-10 17:59:09 UTC
Permalink
Post by Jens Maurer
Here's a proposal to offer a core feature where std::uncaught_exception() fails.
Also available at http://jmaurer.awardspace.info/wg21/destructor-unwinding.html .
Hi,

I'm not a compiler writer and I don't know if this is simple to
implement, but from the user point of view the semantics seems quite clear.
Let me see if I understood it clearly. In the following example

U::~U(int) {
try {
Transaction t( /*...*/ );
// do work
} catch( ... ) {
// clean up
}
}

the destructor of t would call the regular constructor, isn't it?

Been able to define the transaction class should be a must for the
future C++ version.

Vicente

--
Jens Maurer
2012-12-10 22:30:45 UTC
Permalink
Post by Vicente J. Botet Escriba
U::~U(int) {
try {
Transaction t( /*...*/ );
// do work
} catch( ... ) {
// clean up
}
}
the destructor of t would call the regular constructor, isn't it?
Yes, this would call the regular destructor (not constructor) for t.

Jens

--
Ville Voutilainen
2012-12-10 18:21:52 UTC
Permalink
Post by Jens Maurer
Here's a proposal to offer a core feature where std::uncaught_exception() fails.
Also available at http://jmaurer.awardspace.info/wg21/destructor-unwinding.html .
Feedback is welcome.
Can I delegate from one destructor to another?

--
Nevin Liber
2012-12-10 18:38:29 UTC
Permalink
Post by Jens Maurer
Post by Jens Maurer
Here's a proposal to offer a core feature where
std::uncaught_exception() fails.
Post by Jens Maurer
Also available at
http://jmaurer.awardspace.info/wg21/destructor-unwinding.html .
Post by Jens Maurer
Feedback is welcome.
Can I delegate from one destructor to another?
By delegate do you mean delegating all work, or just the "body" of the
destructor, while member variables and base classes are destroyed via their
own ~T(int) destructors.

I'd like to see an explanation of how this is intended to interact (both
user and typical implementation impact) with access specifiers and
virtualness.
--
Nevin ":-)" Liber <mailto:***@eviloverlord.com> (847) 691-1404

--
Ville Voutilainen
2012-12-10 18:46:59 UTC
Permalink
Post by Nevin Liber
Post by Ville Voutilainen
Can I delegate from one destructor to another?
By delegate do you mean delegating all work, or just the "body" of the
destructor, while member variables and base classes are destroyed via their
own ~T(int) destructors.
I think the body. Delegating constructors allow me to get rid of weird
init() functions,
I'd hate to see cleanup() counterpart suddenly coming back because I want to do
things shared between multiple destructors.
Post by Nevin Liber
I'd like to see an explanation of how this is intended to interact (both
user and typical implementation impact) with access specifiers and
virtualness.
"This" as in the delegation question or the feature as proposed by Jens? :)

--
Nevin Liber
2012-12-10 18:47:55 UTC
Permalink
Post by Ville Voutilainen
Post by Nevin Liber
I'd like to see an explanation of how this is intended to interact (both
user and typical implementation impact) with access specifiers and
virtualness.
"This" as in the delegation question or the feature as proposed by Jens? :)
The feature.
--
Nevin ":-)" Liber <mailto:***@eviloverlord.com> (847) 691-1404

--
Ville Voutilainen
2012-12-10 19:06:54 UTC
Permalink
Post by Nevin Liber
I'd like to see an explanation of how this is intended to interact (both
user and typical implementation impact) with access specifiers and
virtualness.
I suppose the same question applies further for cases where one destructor
is user-provided and the other isn't.

--
Jens Maurer
2012-12-10 22:44:27 UTC
Permalink
Post by Nevin Liber
I'd like to see an explanation of how this is intended to interact
(both user and typical implementation impact) with access specifiers
and virtualness.
Access specifiers work as usual, i.e. you first select the function
you want to call, and then you check access. The program is ill-formed
if you don't have access to the function (= destructor) you've chosen.

So, a private unwinding destructor for a block-scope variable will
be ill-formed. (I don't think we want to specify the situations
when no exception can possibly pass through.)

A private unwinding destructor for a subobject will cause any
constructor of the complete object to be ill-formed.

A private unwinding destructor for a complete object would allow
allocation via new and deallocation via delete, but would prohibit
use at block scope.

Virtualness is a bit of a non-issue, because unwinding destructors
are only used for objects at block scope and subobjects that are
both (at that point) essentially non-polymorphic.

So, I think we should outlaw virtual unwinding destructors.

That brings up an interesting topic: Should there be a way
to explicitly invoke an unwinding destructor via "delete" so
that a unique_ptr<> going out of scope could pass on the
unwinding vs. normal scope exit info to its referent? That
seems to be a bit overkill to me.

Jens

--
Jens Maurer
2012-12-10 22:33:45 UTC
Permalink
Post by Ville Voutilainen
Can I delegate from one destructor to another?
No.

If you need this, you need to implement a helper function called
from both destructors.

I'm hoping that your class is sufficiently small (similar to the
Transaction class) that there is essentially no overlap.

Jens


--
Christof Meerwald
2012-12-10 22:34:07 UTC
Permalink
Post by Jens Maurer
Here's a proposal to offer a core feature where std::uncaught_exception() fails.
Also available at http://jmaurer.awardspace.info/wg21/destructor-unwinding.html .
[...]
Post by Jens Maurer
If there is no unwinding destructor in a class and any base or
member has an unwinding destructor, an implicit one is
declared/defined.
Not sure how common it would be for classes to implement unwinding
destructors, but consider the following example:

struct B
{
A a;

~B()
{
do_stuff();
}
};

void foo()
{
B b;
throw 0;
}

If A doesn't implement an unwinding destructur, B's destructor will
call do_stuff, but if A implements an unwinding destructor, ~B(int)
will be implicitly declared/defined and do_stuff won't be called?
Looks a bit surprising to me.


Christof
--
http://cmeerw.org sip:cmeerw at cmeerw.org
mailto:cmeerw at cmeerw.org xmpp:cmeerw at cmeerw.org

--
Jens Maurer
2012-12-10 22:55:45 UTC
Permalink
Post by Christof Meerwald
Not sure how common it would be for classes to implement unwinding
destructors,
It should be a rare case. Except for the transaction classes
alluded to earlier, it can be used to emulate the "D" scope(failure)
and scope(success) features.
Post by Christof Meerwald
struct B
{
A a;
~B()
{
do_stuff();
}
};
void foo()
{
B b;
throw 0;
}
If A doesn't implement an unwinding destructur, B's destructor will
call do_stuff, but if A implements an unwinding destructor, ~B(int)
will be implicitly declared/defined and do_stuff won't be called?
Looks a bit surprising to me.
"A" will be equally surprised if its unwinding destructor doesn't
get called at the right time.

I think deriving from a class that has an unwinding destructor
(or having one as a member) is an even rarer case, so maybe the
above should just be ill-formed unless B has an explicit unwinding
destructor. (Calling a regular destructor from an unwinding one
is ok, but the other way round is not.)

Jens

--
Jean-Marc Bourguet
2012-12-11 08:58:36 UTC
Permalink
Post by Jens Maurer
Post by Christof Meerwald
Not sure how common it would be for classes to implement unwinding
destructors,
It should be a rare case. Except for the transaction classes
alluded to earlier, it can be used to emulate the "D" scope(failure)
and scope(success) features.
Post by Christof Meerwald
struct B
{
A a;
~B()
{
do_stuff();
}
};
void foo()
{
B b;
throw 0;
}
If A doesn't implement an unwinding destructur, B's destructor will
call do_stuff, but if A implements an unwinding destructor, ~B(int)
will be implicitly declared/defined and do_stuff won't be called?
Looks a bit surprising to me.
"A" will be equally surprised if its unwinding destructor doesn't
get called at the right time.
I think deriving from a class that has an unwinding destructor
(or having one as a member) is an even rarer case, so maybe the
above should just be ill-formed unless B has an explicit unwinding
destructor. (Calling a regular destructor from an unwinding one
is ok, but the other way round is not.)
An error would be fine with me as would generating an unwinding
destructor
sharing the same body as the non-unwinding one but called unwinding
destructors
for member and bases (that reminds me what is done to handle virtual
bases;
I'm not knowledgeable about ABI enough to know if it can be reused or
not).
I'd not be fine with generating an unwinding destructor with an empty
body.
It looks too much like an opportunity for errors. Part of me preferred
the error
until I though about templates. I fear that having an error will leads
template
writers to routinely provide an unwinding destructor and my guess is
that it will
often do just the same as the non-unwinding one in its body. But that
will spread
the use of unwinding destructors forcing class writer to do the same
thing. This
could be mitigated by providing a way for template writers to not
provide unwinding
destructors if none is needed (and that would be probably useful even
if there is
an automatically generated unwinding destructor instead of an error).


One probably also need a way to explicitly call the unwinding
destructor for cases
where you separate allocation and lifetime management.

a::~A(1);

is the obvious candidate.

I'm undecided on the related subject of having version of delete
calling the unwinding
destructors, but note that the above facility isn't enough to provide
the functionality
because member operator delete are dynamically dispatched. So

t = new T;
t->~T(1);
operator delete(t);

won't do what is needed. (And (*t).operator delete(t) isn't a valid
alternative).


Yours,
--
Jean-Marc

--
Ville Voutilainen
2012-12-11 09:17:58 UTC
Permalink
Post by Jean-Marc Bourguet
One probably also need a way to explicitly call the unwinding
destructor for cases
where you separate allocation and lifetime management.
a::~A(1);
is the obvious candidate.
I'm undecided on the related subject of having version of delete
calling the unwinding
destructors, but note that the above facility isn't enough to provide
the functionality
because member operator delete are dynamically dispatched. So
t = new T;
t->~T(1);
operator delete(t);
won't do what is needed. (And (*t).operator delete(t) isn't a valid
alternative).
This gives me serious heartburn. And the indication from that burn is that
perhaps it would be better to provide a support library function through which
destructor authors can decide what they will do in a normal destructor.

--
Jens Maurer
2012-12-11 10:16:57 UTC
Permalink
Post by Ville Voutilainen
And the indication from that burn is that
perhaps it would be better to provide a support library function through which
destructor authors can decide what they will do in a normal destructor.
So, you'd be more in favor of a std::called_from_unwinding() that actually
does what we want here? I fear that won't be implementable (easily):
When does the return value of that function change from "true" to "false"?
And back?
Is that a point where the implementation can reasonably inject code to
effect the return value change?

Jens


--
Ville Voutilainen
2012-12-11 10:44:25 UTC
Permalink
Post by Jens Maurer
So, you'd be more in favor of a std::called_from_unwinding() that actually
When does the return value of that function change from "true" to "false"?
And back?
So.. unwinding here, and in the proposal is unwinding that happens as a result
of a throw, so I would expect called_from_unwinding() to return true
after a throw
expression and false when a handler has been entered. I'm not sure
whether that's
exactly right, but that's my (potentially misguided) starting point.
Post by Jens Maurer
Is that a point where the implementation can reasonably inject code to
effect the return value change?
I must admit I don't know. Is that code injection any different from
injecting code
that chooses which destructor to invoke? I guess at the throw site it isn't, and
at the catch site it would be, since your proposal doesn't require
such injection
at the catch site, if I understand correctly.

I can't help but dislike the potential of opening a can of worms if we
have multiple
destructors.

--
Jens Maurer
2012-12-11 11:16:44 UTC
Permalink
Post by Ville Voutilainen
Post by Jens Maurer
So, you'd be more in favor of a std::called_from_unwinding() that actually
When does the return value of that function change from "true" to "false"?
And back?
So.. unwinding here, and in the proposal is unwinding that happens as a result
of a throw, so I would expect called_from_unwinding() to return true
after a throw
expression and false when a handler has been entered.
That's exactly what std::uncaught_exception() does, and that's insufficient
(see Herb's GotW #47 article referred to in the paper). In short,
destructors called during regular scope exit while executing a destructor
due to stack unwinding cannot detect the difference. This deficiency
is the main motivation for the proposal.
Post by Ville Voutilainen
I can't help but dislike the potential of opening a can of worms if we
have multiple
destructors.
An alternative would be to go back to a runtime choice, which probably
kills a few of the worms, including the necessity for "cleanup" functions:

struct S {
~S(bool unwinding); // cannot co-exist with normal destructor in same class
}

S::~S(bool) is called with a "true" argument during stack unwinding and with a
"false" argument otherwise. If a base or member has such a bool destructor,
we call it, forwarding our own argument value. Otherwise, we call the regular
destructor, essentially dropping the difference on the floor. Template wrappers
that want to be flexible would always define a bool destructor. Within the
destructor, the choice between "unwinding" and "regular scope exit" is a
runtime "if", but optimizations such as constant propagation and function cloning
should be able to remove the runtime "if".

It seems this approach would substantially reduce the (compile-time) complexity
and thus the specification complexity, which is good for such a rarely-used
thing.

Jens

--
Ville Voutilainen
2012-12-11 11:56:30 UTC
Permalink
Post by Jens Maurer
Post by Ville Voutilainen
So.. unwinding here, and in the proposal is unwinding that happens as a result
of a throw, so I would expect called_from_unwinding() to return true
after a throw
expression and false when a handler has been entered.
That's exactly what std::uncaught_exception() does, and that's insufficient
(see Herb's GotW #47 article referred to in the paper). In short,
destructors called during regular scope exit while executing a destructor
due to stack unwinding cannot detect the difference. This deficiency
is the main motivation for the proposal.
Ok, this rings a bell, since in Santa Cruz I presented a wishlist item
for being able
to access on-the-fly exceptions, and uncaught_exception +
current_exception can't
do that since they can't know whether the current frame threw. Perhaps we would
need to solve that problem, then, and get multiple benefits rather
than just the rollback
capability.

Please take a look at N2952:
http://open-std.org/JTC1/SC22/WG21/docs/papers/2009/n2952.html

It tries to ruminate on the logging-of-flying-exceptions, perhaps
there are synergies we could
achieve?

--
Jens Maurer
2012-12-11 14:18:14 UTC
Permalink
Post by Ville Voutilainen
http://open-std.org/JTC1/SC22/WG21/docs/papers/2009/n2952.html
It tries to ruminate on the logging-of-flying-exceptions, perhaps
there are synergies we could
achieve?
We could (incompatibly) change the semantics of std::current_exception()
to also work during stack unwinding, and use the new proposed feature
to detect that you're actually called from stack unwinding as opposed
to some other place.

Jens


--
Alberto Ganesh Barbati
2012-12-11 11:19:15 UTC
Permalink
Post by Ville Voutilainen
Post by Jens Maurer
So, you'd be more in favor of a std::called_from_unwinding() that actually
When does the return value of that function change from "true" to "false"?
And back?
So.. unwinding here, and in the proposal is unwinding that happens as a result
of a throw, so I would expect called_from_unwinding() to return true
after a throw
expression and false when a handler has been entered. I'm not sure
whether that's
exactly right, but that's my (potentially misguided) starting point.
Post by Jens Maurer
Is that a point where the implementation can reasonably inject code to
effect the return value change?
I must admit I don't know. Is that code injection any different from
injecting code
that chooses which destructor to invoke? I guess at the throw site it isn't, and
at the catch site it would be, since your proposal doesn't require
such injection
at the catch site, if I understand correctly.
I can't help but dislike the potential of opening a can of worms if we
have multiple
destructors.
Not that I am proposing this, as it clearly has ABI implications that might not be solvable, but why having two destructors? We could have a single destructor and pass the context as a parameter:

~Transaction(bool unwinding) {
if (unwinding)
RollBack();
else
Commit();
}

the value of the parameter is implicitly forwarded to the destructor of subobjects, so that the whole chain is provided the correct value, which is determined by the compiler at the original destruction site. If the declared destructor doesn't take a parameter, it's passed anyway, simply the function doesn't have access to it. It is ill-formed to declare both destructors (with and without parameter).

Given that the destructor is a very special function, the proposed syntax doesn't mean that the parameter has to be actually passed using the usual calling conventions, if that might help addressing ABI issues.

Just an idea,

Ganesh

--
Jens Maurer
2012-12-11 10:25:29 UTC
Permalink
Post by Jean-Marc Bourguet
Post by Jens Maurer
I think deriving from a class that has an unwinding destructor
(or having one as a member) is an even rarer case, so maybe the
above should just be ill-formed unless B has an explicit unwinding
destructor. (Calling a regular destructor from an unwinding one
is ok, but the other way round is not.)
An error would be fine with me as would generating an unwinding
destructor
sharing the same body as the non-unwinding one but called unwinding
destructors
for member and bases (that reminds me what is done to handle virtual
bases;
I'm not knowledgeable about ABI enough to know if it can be reused or
not).
I'd not be fine with generating an unwinding destructor with an empty
body.
It looks too much like an opportunity for errors. Part of me preferred
the error
until I though about templates. I fear that having an error will leads
template
writers to routinely provide an unwinding destructor and my guess is
that it will
often do just the same as the non-unwinding one in its body. But that
will spread
the use of unwinding destructors forcing class writer to do the same
thing. This
could be mitigated by providing a way for template writers to not
provide unwinding
destructors if none is needed (and that would be probably useful even
if there is
an automatically generated unwinding destructor instead of an error).
We might want to have

~S() = default;

generate a regular and an unwinding destructor if and only if they would
differ, i.e. if a base or member has an unwinding destructor. I think
that addresses the template case nicely.
Post by Jean-Marc Bourguet
One probably also need a way to explicitly call the unwinding
destructor for cases
where you separate allocation and lifetime management.
a::~A(1);
is the obvious candidate.
Yes, we need that.
Post by Jean-Marc Bourguet
I'm undecided on the related subject of having version of delete
calling the unwinding
destructors, but note that the above facility isn't enough to provide
the functionality
because member operator delete are dynamically dispatched. So
t = new T;
t->~T(1);
operator delete(t);
won't do what is needed. (And (*t).operator delete(t) isn't a valid
alternative).
I'm inclined not to support that at this time.

Jens

--
Olaf van der Spek
2012-12-10 23:04:04 UTC
Permalink
Post by Jens Maurer
Here's a proposal to offer a core feature where std::uncaught_exception() fails.
Also available at
http://jmaurer.awardspace.info/wg21/destructor-unwinding.html .
Feedback is welcome.
It's just about the example, I hope you don't mind:
What about rolling back the transaction unless explicitly commited?
Seems cleaner and safer, no unexpected commits.

--
Jens Maurer
2012-12-11 10:19:34 UTC
Permalink
Post by Olaf van der Spek
What about rolling back the transaction unless explicitly commited?
That's a design choice you can make. With current C++11, it seems to
be the only reasonable design choice.
Post by Olaf van der Spek
Seems cleaner and safer, no unexpected commits.
Well, that depends on your expectations, I'd guess. A part of the job
of the C++ language specification is to induce appropriate
expectations :-)

Jens

--
Olaf van der Spek
2012-12-17 13:41:24 UTC
Permalink
Post by Jens Maurer
Post by Olaf van der Spek
What about rolling back the transaction unless explicitly commited?
That's a design choice you can make. With current C++11, it seems to
be the only reasonable design choice.
If a reasonable solution exist, is it still worth all the trouble to try to
support the other choice?
--
Jens Maurer
2012-12-17 19:36:49 UTC
Permalink
Post by Jens Maurer
Post by Olaf van der Spek
What about rolling back the transaction unless explicitly commited?
That's a design choice you can make. With current C++11, it seems to
be the only reasonable design choice.
If a reasonable solution exist, is it still worth all the trouble to try to support the other choice?
Good question.

Possible answers:

- Note that intermediate "return" or "break" statements could make
you leave the transaction scope with the intention to commit.
Requiring every such code path to explicitly say "commit" seems
error-prone; you might overlook a rarely-executed code path.

- Given that the programmer already has to write "transaction t(...)" in her
program, why is it necessary to explicitly state "t.commit()"? This violates
a bit the RAII idiom (which is only approximately applicable here).

- I think there are situations where you exactly need to distinguish
whether throwing from a destructor invocation will surely call std::terminate,
or not.

- Emulation of the "D" language scope(exit) and scope(failure) features
requires the differentiation.

Jens

--
Zhihao Yuan
2012-12-17 21:57:40 UTC
Permalink
Post by Jens Maurer
- Note that intermediate "return" or "break" statements could make
you leave the transaction scope with the intention to commit.
Requiring every such code path to explicitly say "commit" seems
error-prone; you might overlook a rarely-executed code path.
"Commit" is different from releasing; you only need it at _one_ safe place, not
multiple unsafe places. It works for both local and non-local jump.
Post by Jens Maurer
- Emulation of the "D" language scope(exit) and scope(failure) features
requires the differentiation.
scope(failure) is nice, but it's not that easy to use (and hard to
read...). Since it
only works for non-local jump, you have to protect local jump (non-exception
failures) by yourself. A "commit()" can be more explicit.

However, I do +1 for your idea and even throwable destructors. A detectable
unwinding makes things clean, that's my feeling.

--
Zhihao Yuan, ID lichray
The best way to predict the future is to invent it.
___________________________________________________
4BSD -- http://4bsd.biz/

--
Olaf van der Spek
2012-12-18 00:03:57 UTC
Permalink
X-Received: by 10.50.33.236 with SMTP id u12mr163094igi.2.1355789038688;
Mon, 17 Dec 2012 16:03:58 -0800 (PST)
X-BeenThere: std-***@isocpp.org
Received: by 10.50.5.175 with SMTP id t15ls4017514igt.33.canary; Mon, 17 Dec
2012 16:03:58 -0800 (PST)
X-Received: by 10.42.51.142 with SMTP id e14mr179699icg.2.1355789038280;
Mon, 17 Dec 2012 16:03:58 -0800 (PST)
X-Received: by 10.42.51.142 with SMTP id e14mr179698icg.2.1355789038264;
Mon, 17 Dec 2012 16:03:58 -0800 (PST)
Received: from mail-ia0-f173.google.com (mail-ia0-f173.google.com [209.85.210.173])
by mx.google.com with ESMTPS id we8si8209277igb.13.2012.12.17.16.03.58
(version=TLSv1/SSLv3 cipher=OTHER);
Mon, 17 Dec 2012 16:03:58 -0800 (PST)
Received-SPF: pass (google.com: domain of ***@gmail.com designates 209.85.210.173 as permitted sender) client-ip=209.85.210.173;
Received: by mail-ia0-f173.google.com with SMTP id w21so5885988iac.18
for <std-***@isocpp.org>; Mon, 17 Dec 2012 16:03:58 -0800 (PST)
Received: by 10.42.131.133 with SMTP id z5mr155814ics.10.1355789038159; Mon,
17 Dec 2012 16:03:58 -0800 (PST)
Received: by 10.50.184.226 with HTTP; Mon, 17 Dec 2012 16:03:57 -0800 (PST)
In-Reply-To: <***@gmx.net>
X-Original-Sender: ***@gmail.com
X-Original-Authentication-Results: mx.google.com; spf=pass (google.com: domain
of ***@gmail.com designates 209.85.210.173 as permitted sender)
smtp.mail=***@gmail.com; dkim=pass header.i=@gmail.com
Precedence: list
Mailing-list: list std-***@isocpp.org; contact std-proposals+***@isocpp.org
List-ID: <std-proposals.isocpp.org>
X-Google-Group-Id: 399137483710
List-Post: <http://groups.google.com/a/isocpp.org/group/std-proposals/post?hl=en>,
<mailto:std-***@isocpp.org>
List-Help: <http://support.google.com/a/isocpp.org/bin/topic.py?hl=en&topic=25838>,
<mailto:std-proposals+***@isocpp.org>
List-Archive: <http://groups.google.com/a/isocpp.org/group/std-proposals/?hl=en>
List-Subscribe: <http://groups.google.com/a/isocpp.org/group/std-proposals/subscribe?hl=en>,
<mailto:std-proposals+***@isocpp.org>
List-Unsubscribe: <http://groups.google.com/a/isocpp.org/group/std-proposals/subscribe?hl=en>,
<mailto:googlegroups-manage+399137483710+***@googlegroups.com>
Archived-At: <http://permalink.gmane.org/gmane.comp.lang.c++.isocpp.proposals/1063>
Post by Jens Maurer
Post by Jens Maurer
Post by Olaf van der Spek
What about rolling back the transaction unless explicitly commited?
That's a design choice you can make. With current C++11, it seems to
be the only reasonable design choice.
If a reasonable solution exist, is it still worth all the trouble to try to support the other choice?
Good question.
- Note that intermediate "return" or "break" statements could make
you leave the transaction scope with the intention to commit.
Or the intention could be to rollback.
Post by Jens Maurer
Requiring every such code path to explicitly say "commit" seems
error-prone; you might overlook a rarely-executed code path.
Usually there are more error exits and less non-error exits.
Non-error exits also tend to be tested better.

What's worse? An accidentally commited transaction or an accidentally
rolled back one?
Post by Jens Maurer
- Given that the programmer already has to write "transaction t(...)" in her
program, why is it necessary to explicitly state "t.commit()"?
Because there's a choice: commit or rollback. And IMO there's only one
safe default.
Post by Jens Maurer
This violates
a bit the RAII idiom (which is only approximately applicable here).
That's about releasing resources, not about committing or rolling back.
Post by Jens Maurer
- I think there are situations where you exactly need to distinguish
whether throwing from a destructor invocation will surely call std::terminate,
or not.
- Emulation of the "D" language scope(exit) and scope(failure) features
requires the differentiation.
Copying features without looking at the problems they solve isn't good design.
--
Olaf

--
Aleksandar Fabijanic
2012-12-17 21:59:39 UTC
Permalink
Post by Olaf van der Spek
Post by Jens Maurer
Post by Olaf van der Spek
What about rolling back the transaction unless explicitly commited?
That's a design choice you can make. With current C++11, it seems to
be the only reasonable design choice.
If a reasonable solution exist, is it still worth all the trouble to try to
support the other choice?
This is a real world problem and current solutions do not quite cut it
all the way. I'd go further than original proposal - what would be
really valuable is, instead of dummy int, to have the actual exception
passed to the destructor - before deciding and attempting
commit/rollback, you'd want to know whether it makes sense to attempt
either. For example, if the uncaught exception was due to loss of
database connection then attempting neither makes sense. If there is a
destructor with argument that matches (rules same as for catch()
matching) the exception thrown, it is called; if not, the default
destructor is called.

~T() { // clean scope exit
try { commit(); }
catch(...) { }
}

~T(const std::exception& e) {
try { rollback(); }
catch(...) { }
}

~T(const conn_lost_exc& c) {
// neither rollback nor commit make sense ...
}

I have no idea how difficult would this be to implement, but it would
be really nice to have.

Alex

--
Nevin Liber
2012-12-17 22:03:32 UTC
Permalink
On 17 December 2012 15:59, Aleksandar Fabijanic
Post by Aleksandar Fabijanic
This is a real world problem and current solutions do not quite cut it
all the way. I'd go further than original proposal - what would be
really valuable is, instead of dummy int, to have the actual exception
passed to the destructor - before deciding and attempting
commit/rollback, you'd want to know whether it makes sense to attempt
either.
That would be interesting...
Post by Aleksandar Fabijanic
For example, if the uncaught exception was due to loss of
database connection then attempting neither makes sense. If there is a
destructor with argument that matches (rules same as for catch()
matching) the exception thrown, it is called; if not, the default
destructor is called.
So, you are proposing that declaration order of destructors would matter
(since that is what happens with catch clauses)?
--
Nevin ":-)" Liber <mailto:***@eviloverlord.com> (847) 691-1404

--
Aleksandar Fabijanic
2012-12-17 22:22:12 UTC
Permalink
Post by Nevin Liber
So, you are proposing that declaration order of destructors would matter
(since that is what happens with catch clauses)?
I'd prefer it to find a best match regardless of order but whether
that is feasible woud be a question for someone well versed in
compiler implementation.

--
Klaim - Joël Lamotte
2012-12-17 22:05:08 UTC
Permalink
On Mon, Dec 17, 2012 at 10:59 PM, Aleksandar Fabijanic <
Post by Aleksandar Fabijanic
I have no idea how difficult would this be to implement, but it would
be really nice to have.
Doesn't it force the compiler to generate try/catch code for all
destructors?

Joel Lamotte

--
Aleksandar Fabijanic
2012-12-17 22:27:37 UTC
Permalink
Post by Klaim - Joël Lamotte
On Mon, Dec 17, 2012 at 10:59 PM, Aleksandar Fabijanic
Post by Aleksandar Fabijanic
I have no idea how difficult would this be to implement, but it would
be really nice to have.
Doesn't it force the compiler to generate try/catch code for all
destructors?
At the place where regular scope exit destructor is called, things
would be the same as they've always been. Elsewhere, wherever the
destructor is invoked, there would have to be logic to decide which
one exactly to invoke.

Alex

--
s***@hotmail.com
2012-12-15 10:09:04 UTC
Permalink
I like the idea, but the syntax is a bit ugly. I'd prefer something like
this:
struct foo
{
~foo() { ... } // regular destructor
!foo() { ... } // unwinding destructor
};

The runtime choice with a bool parameter seems to introduce unnecessary
overhead.

--
Jens Maurer
2012-12-15 17:10:46 UTC
Permalink
||
structfoo
{
~foo(){...}// regular destructor
!foo(){...}// unwinding destructor
};
The runtime choice with a bool parameter seems to introduce unnecessary overhead.
Earlier in the thread, we discussed
~foo(int){...} // unwinding destructor

which, we believe, has more fallout throughout the language
than the bool parameter. Your !foo() suggestion is equivalent
to that. Since this is a feature that will be used in rare cases
only, we should keep the fallout minimal.

I'm sure a compiler can optimize the bool parameter away
by constant propagation and function cloning.

Jens

--
Daniel Krügler
2012-12-15 17:35:46 UTC
Permalink
Post by Jens Maurer
Earlier in the thread, we discussed
~foo(int){...} // unwinding destructor
which, we believe, has more fallout throughout the language
than the bool parameter. Your !foo() suggestion is equivalent
to that. Since this is a feature that will be used in rare cases
only, we should keep the fallout minimal.
I'm sure a compiler can optimize the bool parameter away
by constant propagation and function cloning.
I agree that ~foo(int) looks like a consistent choice for C++.

Btw.: It just occurs to me that destructors are not automatically
handled by the general wording for operators, 13.5 p8

"An operator function cannot have default arguments (8.3.6), except
where explicitly stated below."

so the question arises whether we want to allow for

~foo(int = 0){...}

which would call a different destructor for current explicit
destructor calls. At the moment I tend to disallows a default argument
here.

- Daniel

--
Jens Maurer
2012-12-15 18:34:49 UTC
Permalink
Post by Daniel Krügler
I agree that ~foo(int) looks like a consistent choice for C++.
Right, but it seems to have more fallout than using
~foo(bool)
as the new-style destructor where the value of the parameter
indicates "unwinding" or not. So, in a sense, traditional
~foo() destructor could become deprecated.

And no, we don't want default arguments.

Jens

--
Fernando Pelliccioni
2012-12-17 04:07:30 UTC
Permalink
I wonder if it makes sense the concept of "unwinding destructor" if
something like this were to be approved:

http://cpp-next.com/archive/2012/08/evil-or-just-misunderstood/
Post by Jens Maurer
Post by Daniel Krügler
I agree that ~foo(int) looks like a consistent choice for C++.
Right, but it seems to have more fallout than using
~foo(bool)
as the new-style destructor where the value of the parameter
indicates "unwinding" or not. So, in a sense, traditional
~foo() destructor could become deprecated.
And no, we don't want default arguments.
Jens
--
--
Jens Maurer
2012-12-17 08:33:25 UTC
Permalink
Post by Fernando Pelliccioni
http://cpp-next.com/archive/2012/08/evil-or-just-misunderstood/
The main article just presents the problem, and concludes:

"This being C++, we expect someone to want more control over that second
exception, so in our next installment, we’ll consider some alternatives."

So there isn't actually a suggestion in there to approve.

I didn't wade through the forest of comments.

Jens

--
Daniel Krügler
2012-12-15 17:24:31 UTC
Permalink
Post by s***@hotmail.com
I like the idea, but the syntax is a bit ugly. I'd prefer something like
struct foo
{
~foo() { ... } // regular destructor
!foo() { ... } // unwinding destructor
};
Using this choice would break a popular language extension ("managed C++"),
where the above unwinding destructor would refer to a so-called finalizer:

http://msdn.microsoft.com/en-us/library/vstudio/ms177197.aspx

Usually the C++ standard committee tries to minimize such kind of damage to
existing extensions.

Also, I think that the form suggested by Jens (allowing for some parameter type
to be discussed) looks quite natural for the C++ language.

- Daniel

--
Sebastian Gesemann
2012-12-17 12:42:56 UTC
Permalink
Post by Jens Maurer
Here's a proposal to offer a core feature where
std::uncaught_exception() fails. Also available at
http://jmaurer.awardspace.info/wg21/destructor-unwinding.html .
struct Transaction {
Transaction();
~Transaction() {
if (std::uncaught_exception())
RollBack();
else
Commit();
}
};
The well-intended meaning was for a transaction to roll back if its
scope was exited via an exception. However, this detection is
U::~U(int) {
try {
Transaction t( /*...*/ );
// do work
} catch( ... ) {
// clean up
}
}
If U::~U() is called during stack unwinding, the transaction inside
will always roll back, although "commit" was expected. Note that the
the transaction construct could appear in a called function that is
not (and should not be) aware of the context from which it is called.
If there was a way to compare uncaught exceptions, there would be
another solution:

- ask runtime about an uncaught exception during the
CONSTRUCTION of the Transaction object

- ask runtime about an uncaught exception during the
DESTRUCTION of the Transaction object

- If we have a new uncaught exception at destruction
(different from a possible one at construction time)
we're in stack-unwinding mode.

for now, this does not seem possible because std::current_exception
only returns a non-null exception_ptr for HANDLED exceptions and not
for uncaught exceptions, as far as I can tell. Otherwise we could write
something like this:

class UnwindDetector
{
public:
UnwindDetector();
: atinit(std::current_exception())
{}
bool unwinding() const
{ return std::uncaught_exception()
&& (atinit != std::current_exception());
}
private:
std::exception_ptr atinit;
};

struct Transaction
{
UnwindDetector uwd;
Transaction();
~Transaction()
{ if (uwd.unwinding())
RollBack();
else
Commit();
}
};

Cheers!
SG

--
Fernando Cacciola
2012-12-21 16:23:03 UTC
Permalink
X-Received: by 10.182.36.35 with SMTP id n3mr2678633obj.19.1356107024140;
Fri, 21 Dec 2012 08:23:44 -0800 (PST)
X-BeenThere: std-***@isocpp.org
Received: by 10.182.113.67 with SMTP id iw3ls373992obb.15.gmail; Fri, 21 Dec
2012 08:23:43 -0800 (PST)
X-Received: by 10.60.29.226 with SMTP id n2mr11196844oeh.132.1356107023764;
Fri, 21 Dec 2012 08:23:43 -0800 (PST)
X-Received: by 10.60.29.226 with SMTP id n2mr11196842oeh.132.1356107023750;
Fri, 21 Dec 2012 08:23:43 -0800 (PST)
Received: from mail-oa0-f52.google.com (mail-oa0-f52.google.com [209.85.219.52])
by mx.google.com with ESMTPS id s5si12455528obo.196.2012.12.21.08.23.43
(version=TLSv1/SSLv3 cipher=OTHER);
Fri, 21 Dec 2012 08:23:43 -0800 (PST)
Received-SPF: pass (google.com: domain of ***@gmail.com designates 209.85.219.52 as permitted sender) client-ip=209.85.219.52;
Received: by mail-oa0-f52.google.com with SMTP id o6so4721304oag.25
for <std-***@isocpp.org>; Fri, 21 Dec 2012 08:23:43 -0800 (PST)
Received: by 10.60.171.175 with SMTP id av15mr11328127oec.75.1356107023617;
Fri, 21 Dec 2012 08:23:43 -0800 (PST)
Received: by 10.182.228.66 with HTTP; Fri, 21 Dec 2012 08:23:03 -0800 (PST)
In-Reply-To: <CAGdQazd6UVBYYZUyfYEvSUkpj=CoSn+cNQ=A=***@mail.gmail.com>
X-Original-Sender: ***@gmail.com
X-Original-Authentication-Results: mx.google.com; spf=pass (google.com: domain
of ***@gmail.com designates 209.85.219.52 as permitted sender)
smtp.mail=***@gmail.com; dkim=pass header.i=@gmail.com
Precedence: list
Mailing-list: list std-***@isocpp.org; contact std-proposals+***@isocpp.org
List-ID: <std-proposals.isocpp.org>
X-Google-Group-Id: 399137483710
List-Post: <http://groups.google.com/a/isocpp.org/group/std-proposals/post?hl=en>,
<mailto:std-***@isocpp.org>
List-Help: <http://support.google.com/a/isocpp.org/bin/topic.py?hl=en&topic=25838>,
<mailto:std-proposals+***@isocpp.org>
List-Archive: <http://groups.google.com/a/isocpp.org/group/std-proposals/?hl=en>
List-Subscribe: <http://groups.google.com/a/isocpp.org/group/std-proposals/subscribe?hl=en>,
<mailto:std-proposals+***@isocpp.org>
List-Unsubscribe: <http://groups.google.com/a/isocpp.org/group/std-proposals/subscribe?hl=en>,
<mailto:googlegroups-manage+399137483710+***@googlegroups.com>
Archived-At: <http://permalink.gmane.org/gmane.comp.lang.c++.isocpp.proposals/1195>

On Mon, Dec 17, 2012 at 9:42 AM, Sebastian Gesemann
Post by Sebastian Gesemann
class UnwindDetector
{
UnwindDetector();
: atinit(std::current_exception())
{}
bool unwinding() const
{ return std::uncaught_exception()
&& (atinit != std::current_exception());
}
std::exception_ptr atinit;
};
struct Transaction
{
UnwindDetector uwd;
Transaction();
~Transaction()
{ if (uwd.unwinding())
RollBack();
else
Commit();
}
};
Interesting...

I like the original proposal of having two destructors because it is
quite clear what it is about.
And then I like the form taking the bool such that the regular dtor
would not even be implemented when the other is needed.

However, this solution would also get the job done AFAICT. And it also
provides a way to determine what exactly is happening, like Aleksandar
proposed, even if via RTTI as opposed to a direct dispatching resolved
by the compiler. So, it seems to me that this is a better solution.

Lastly, I've been wondering if there could be a use case when I am in
a normal scope exit destructor BUT within an unwinding destructor, and
I want to know that and use a strategy closer to the unwinding than
the normal exit. In that case, the separate dtros would get in the way
while this solution would give me all the information I want.



--
Fernando Cacciola
SciSoft Consulting, Founder
http://www.scisoft-consulting.com

--
Jens Maurer
2012-12-22 07:10:33 UTC
Permalink
Post by Sebastian Gesemann
for now, this does not seem possible because std::current_exception
only returns a non-null exception_ptr for HANDLED exceptions and not
for uncaught exceptions, as far as I can tell. Otherwise we could write
class UnwindDetector
{
UnwindDetector();
: atinit(std::current_exception())
{}
bool unwinding() const
{ return std::uncaught_exception()
&& (atinit != std::current_exception());
}
std::exception_ptr atinit;
};
std::current_exception() may return a copy of the original exception,
so the comparison might not do the right thing.

It seems this approach is fragile.

Jens


--
Alberto Barbati
2012-12-22 17:39:51 UTC
Permalink
Post by Jens Maurer
Post by Sebastian Gesemann
for now, this does not seem possible because std::current_exception
only returns a non-null exception_ptr for HANDLED exceptions and not
for uncaught exceptions, as far as I can tell. Otherwise we could write
class UnwindDetector
{
UnwindDetector();
: atinit(std::current_exception())
{}
bool unwinding() const
{ return std::uncaught_exception()
&& (atinit != std::current_exception());
}
std::exception_ptr atinit;
};
std::current_exception() may return a copy of the original exception,
so the comparison might not do the right thing.
It seems this approach is fragile.
It is fragile, but it is still very insightful. First of all, I believe the following easier approach should actually work:

struct Transaction
{
bool uncaught_exception_at_ctor;
Transaction()
{
uncaught_exception_at_ctor = uncaught_exception();
}
~Transaction()
{ if (uncaught_exception() and not uncaught_exception_at_ctor)
RollBack();
else
Commit();
}
};

Notice that if uncaught_exception_at_ctor is true, Commit() is always called. That is based on the assumption that in that case the dtor will never be invoked as part of a stack unwinding: any throw that might trigger it would call terminate() instead.

Even if my approach is proved not to work, the example shows that a pure library approach might actually be implementable. The key point is that the facility has to be splitted in two functions: one to call in the ctor and one in the dtor. An alternative design could be similar to class UnwindDetector itself.

Just my two eurocent,

Ganesh

--
Alberto Barbati
2012-12-23 09:19:33 UTC
Permalink
Post by Jens Maurer
struct Transaction
{
bool uncaught_exception_at_ctor;
Transaction()
{
uncaught_exception_at_ctor = uncaught_exception();
}
~Transaction()
{ if (uncaught_exception() and not uncaught_exception_at_ctor)
RollBack();
else
Commit();
}
};
I take it back. It does not work, I don't know what was I thinking... However, if the implementation could maintain a count of the exceptions thrown but not yet handled (instead of the mere boolean retrieved via uncaught_exception) we might write this:

struct Transaction
{
bool ue_count_at_ctor;
Transaction()
{
ue_count_at_ctor = uncaught_exception_count();
}
~Transaction()
{ if (ue_count_at_ctor < uncaught_exception_count())
RollBack();
else
Commit();
}
};

I believe uncaught_exception_count() could be implementable without significant added cost: just increment a per-thread counter upon throw and decrement it upon entering a handler. Is this approach worth considering?

Ganesh

--
s***@gmail.com
2012-12-23 18:36:17 UTC
Permalink
Post by Alberto Barbati
believe uncaught_exception_count() could be implementable without
significant added cost: just increment a per-thread counter upon throw and
decrement it upon entering a handler. Is this approach worth considering?
Actually most compilers already maintain this count internally. This is
used by https://github.com/panaseleus/stack_unwinding to implement
something like UnwindDetector (in a currently nonstandard way).

In https://github.com/panaseleus/stack_unwinding/blob/master/boost/exception/uncaught_exception_count.hpp
you can see how uncaught_exception_count is implemented for msvc/gcc/clang.
This depends on internal compiler data-structures and such, but maybe it
should not be that difficult to standardize this into something like
std::uncaught_exception_count?

(Not this isn't my work, i just found it on the boost mailing list once and
thought it would be relevant here)

--
Nikolay Ivchenkov
2012-12-26 14:25:24 UTC
Permalink
Post by Jens Maurer
Here's a proposal to offer a core feature where std::uncaught_exception() fails.
Also available at
http://jmaurer.awardspace.info/wg21/destructor-unwinding.html .
I don't see any reasons why a class should be responsible for partial
handling of unrelated exceptions in block scopes. In general, only a user
of a class can decide what to do when an automatic variable goes out of its
scope due to a particular circumstance. So, I believe that a user (not a
class developer) should have simple and convenient mechanism to handle
scope leaving. Such a mechanism is already invented in D.

For example, if we want to call 'commit' when control leaves a particular
scope "normally" (not due to an exception), we could write:

struct Transaction
{
Transaction();
Transcation(Transaction &&x) noexcept :
/*...*/
{
x.committed = true;
}
void commit()
{
/*...*/
committed = true;
}
void roll_back() noexcept;

~Transaction()
{
if (!committed)
roll_back();
}

bool committed = false;
/*...*/
};

void f()
{
Transaction transaction;
scope_success { transaction.commit(); }
/*...*/
}

In some other scope evaluation of every return statement might be
interpreted as failure (as well as throwing an exception), so we could
write:

void g()
{
Transaction transaction;
/*...*/
transaction.commit();
}

In both cases we have simple clear self-documenting code.

--
Jens Maurer
2012-12-26 21:11:44 UTC
Permalink
Post by Jens Maurer
Here's a proposal to offer a core feature where std::uncaught_exception() fails.
Also available at http://jmaurer.awardspace.info/wg21/destructor-unwinding.html <http://jmaurer.awardspace.info/wg21/destructor-unwinding.html> .
In general, only a
user of a class can decide what to do when an automatic variable goes
out of its scope due to a particular circumstance.
Well, right now the class decides: The destructor is called, and the user
has no further influence on that.
Post by Jens Maurer
So, I believe that
a user (not a class developer) should have simple and convenient
mechanism to handle scope leaving. Such a mechanism is already
invented in D.
For example, if we want to call 'commit' when control leaves a
particular scope "normally" (not due to an exception), we could
That's the way we would write a Transaction class in C++11, and
force the user to call "commit()" explicitly on each "success" scope
exit.
Post by Jens Maurer
void f()
{
Transaction transaction;
scope_success { transaction.commit(); }
/*...*/
}
My proposal would make it possible to write such a scope_success
class, taking a lambda, if it were your preferred programming
style. Personally, I'd like to tie initialization of the transaction
and success / failure together inside the class, and not require
the user to remember whether rollback() was called roll_back().
Post by Jens Maurer
In both cases we have simple clear self-documenting code.
Well, the rollback is implicit in both cases. We seem to ask the
question whether the "commit" should be explicitly stated by the
user or implied by a non-exceptional return. I can see uses for
both ways, but the "implied" option doesn't exist in C++11.

Jens

--
Nikolay Ivchenkov
2012-12-27 21:56:44 UTC
Permalink
Post by Jens Maurer
Well, right now the class decides: The destructor is called, and the user
has no further influence on that.
If I want to prevent execution of a destructor, I'm free to create objects
manually via new-expressions. In the vast majority of cases automatic
destructor calls are desirable.
Post by Jens Maurer
Post by Nikolay Ivchenkov
void f()
{
Transaction transaction;
scope_success { transaction.commit(); }
/*...*/
}
My proposal would make it possible to write such a scope_success
class, taking a lambda, if it were your preferred programming
style.
I already use such classes:

struct scope_failure : runtime_error
{
char const *what() const noexcept override
{
return "Conditional scope statement is reached during stack
unwinding";
}
char const *exception_name() const noexcept override
{
return "::nstdx::scope_failure";
}
};

template <class F>
class scope_success_wrapper
{
public:
scope_success_wrapper(F &&f) :
m_f(NSTDX_FWD(f))
{
if (std::uncaught_exception())
{
throw scope_failure();
}
}
~scope_success_wrapper() noexcept(false)
{
if (!std::uncaught_exception())
m_f();
}
private:
F m_f;
};

struct scope_success_wrapper_maker {};

template <class F>
scope_success_wrapper<F> operator <<(scope_success_wrapper_maker, F
&&f)
{ return NSTDX_FWD(f); }

#define
NSTDX_SCOPE_SUCCESS(...)
\
auto NSTDX_CONCAT(NSTDX_NSTDX_SCOPE_SUCCESS_VAR_, __LINE__)
= \
::nstdx::scope_success_wrapper_maker() << [__VA_ARGS__]() ->
void

void example()
{
Transaction t;
NSTDX_SCOPE_SUCCESS(&) { t.commit(); }
/*...*/
}

Although during stack unwinding execution of blocks with
NSTDX_SCOPE_SUCCESS would always fail, this is mostly theoretical issue,
because usually (almost always?) destructors don't have such a complex
logic inside. In oder to make this technique theoretically perfect (not
considering potential performance penalties), it would be sufficient to use
(currently non-existing) function std::uncaught_exception_count that would
return number of currently active exceptions.

Well, the rollback is implicit in both cases.


Calling rollback seems to be a safe default behavior.

We seem to ask the
Post by Jens Maurer
question whether the "commit" should be explicitly stated by the
user or implied by a non-exceptional return.
Objects may have static or thread storage duration, Does it make sense to
consider invocation of their destructors as a result of a successful
execution? And is it safe (functions like "commit" may throw exceptions)?

--
Nikolay Ivchenkov
2012-12-28 09:30:20 UTC
Permalink
Post by Jens Maurer
Well, right now the class decides: The destructor is called, and the user
has no further influence on that.
If I want to prevent execution of a destructor, I'm free to create objects
manually via new-expressions. In the vast majority of cases automatic
destructor calls are desirable.
Post by Jens Maurer
Post by Nikolay Ivchenkov
void f()
{
Transaction transaction;
scope_success { transaction.commit(); }
/*...*/
}
My proposal would make it possible to write such a scope_success
class, taking a lambda, if it were your preferred programming
style.
I already use such classes:

namespace nstdx
{
struct scope_failure : runtime_error
{
char const *what() const noexcept override
{
return "Conditional scope-statement is reached during stack
unwinding";
}
char const *exception_name() const noexcept override
{
return "::nstdx::scope_failure";
}
};

template <class F>
class scope_success_wrapper
{
public:
scope_success_wrapper(F &&f) :
m_f(NSTDX_FWD(f))
{
if (std::uncaught_exception())
{
throw scope_failure();
}
}
~scope_success_wrapper() noexcept(false)
{
if (!std::uncaught_exception())
m_f();
}
scope_success_wrapper(scope_success_wrapper const &) = delete;
private:
F m_f;
};

struct scope_success_wrapper_maker {};

template <class F>
scope_success_wrapper<F> operator
<<(scope_success_wrapper_maker, F &&f)
{ return {NSTDX_FWD(f)}; }
}

#define
NSTDX_SCOPE_SUCCESS(...)
\
auto &&NSTDX_CONCAT(NSTDX_NSTDX_SCOPE_SUCCESS_VAR_, __LINE__)
= \
::nstdx::scope_success_wrapper_maker() << [__VA_ARGS__]() ->
void

void example()
{
Transaction t;
NSTDX_SCOPE_SUCCESS(&) { t.commit(); };
/*...*/
}

During stack unwinding execution of blocks with NSTDX_SCOPE_SUCCESS would
always fail. I consider this as mostly theoretical issue, because usually
(almost always?) destructors don't have such a complex logic inside. In
oder to make this technique theoretically perfect (not considering
potential performance penalties), it would be sufficient to use (currently
non-existing) function std::uncaught_exception_count that would return
number of currently active exceptions.
Post by Jens Maurer
Well, the rollback is implicit in both cases.
Calling rollback seems to be a safe default behavior.
Post by Jens Maurer
We seem to ask the
question whether the "commit" should be explicitly stated by the
user or implied by a non-exceptional return.
Objects may have static or thread storage duration, Does it make sense to
consider invocation of destructor for a thread_local variable as a result
of a successful execution? And is it safe to throw exceptions from
destructors of objects with unknown storage duration (note that functions
like "commit" may throw exceptions)?

--
Jens Maurer
2012-12-31 16:57:49 UTC
Permalink
Post by Nikolay Ivchenkov
Objects may have static or thread storage duration, Does it make
sense to consider invocation of destructor for a thread_local
variable as a result of a successful execution?
I believe, yes. (If you mean "not stack unwinding"
when talking about "successful execution".)
Post by Nikolay Ivchenkov
And is it safe to
throw exceptions from destructors of objects with unknown storage
duration (note that functions like "commit" may throw exceptions)?
It's not; throwing from a thread_local destructor calls std::terminate
(see 15.5.1p1 and 3.6.3p1).

There are other, probably more severe restrictions on static and thread
storage duration variables, for example if their construction exits
via an exception, std::terminate is called, too (3.6.2p6).

I don't think that offering the design option for block-scope
objects negatively affects static and thread storage duration uses
in a way that wasn't there before.

Jens

--
Nikolay Ivchenkov
2013-01-01 11:16:50 UTC
Permalink
Post by Jens Maurer
Post by Nikolay Ivchenkov
Objects may have static or thread storage duration, Does it make
sense to consider invocation of destructor for a thread_local
variable as a result of a successful execution?
I believe, yes. (If you mean "not stack unwinding"
when talking about "successful execution".)
I want to understand how you define "successful execution" (as a condition
when it would be reasonable to execute operations like "commit"). I don't
consider the equivalence between "successful execution" and "not stack
unwinding" as self-evident.
Post by Jens Maurer
And is it safe to
Post by Nikolay Ivchenkov
throw exceptions from destructors of objects with unknown storage
duration (note that functions like "commit" may throw exceptions)?
It's not; throwing from a thread_local destructor calls std::terminate
(see 15.5.1p1 and 3.6.3p1).
There are other, probably more severe restrictions on static and thread
storage duration variables, for example if their construction exits
via an exception, std::terminate is called, too (3.6.2p6).
There are significant differences between construction and destruction with
regard to exceptions:

1) construction of a complete object always takes place when the control
enters some visible instruction in the program: variable definition,
explicit type conversion, initialization of a function parameter,
new-expression, so the exact moment of construction of a particular object
is directly visible to a programmer; destructors of objects with
non-dynamic storage duration are called implicitly and such implicit calls
have more chances to be forgotten;

2) exceptions thrown from constructors of local variables with static or
thread stroage duration can be handled; we can't surround destruction of
such objects by any try-blocks.

IMO, throwing destructors are much more error-prone than throwing
constructors.

--
Jens Maurer
2013-01-01 20:03:33 UTC
Permalink
Post by Nikolay Ivchenkov
I want to understand how you define "successful execution" (as a
condition when it would be reasonable to execute operations like
"commit"). I don't consider the equivalence between "successful
execution" and "not stack unwinding" as self-evident.
I don't think it's reasonable to have a static or thread storage
duration "transaction"-style object, at all, so I don't think the
question when to call "commit" arises in that context.
Post by Nikolay Ivchenkov
1) construction of a complete object always takes place when the
control enters some visible instruction in the program: variable
definition, explicit type conversion, initialization of a function
parameter, new-expression, so the exact moment of construction of a
particular object is directly visible to a programmer
I disagree. The order of construction of non-local static or thread
storage duration objects is unspecified and often surprising to the
programmer, and there is not really any program source code corresponding
to the "exact moment of construction".
Post by Nikolay Ivchenkov
; destructors of
objects with non-dynamic storage duration are called implicitly and
such implicit calls have more chances to be forgotten;
2) exceptions thrown from constructors of local variables with static
or thread stroage duration can be handled; we can't surround
destruction of such objects by any try-blocks.
Exceptions from constructors of non-local variables with static or
thread storage duration can't be handled.
Post by Nikolay Ivchenkov
IMO, throwing destructors are much more error-prone than throwing constructors.
Sure. I think the "throwing" aspect is somewhat independent of the
"commit vs. rollback" aspect; I could envision situations where a
"commit" doesn't throw an exception, either.

Jens

--

Loading...