Topic: Copy elision, move constructors, and catching by value


Author: wasti.redl@gmx.net
Date: Wed, 1 Apr 2009 18:36:11 CST
Raw View
Hi,

12.8p15 of N2857, i.e. the most recent draft, specifies the
circumstances under which copy elision is permitted. The fourth bullet
says:

"when the exception-declaration of an exception handler declares an
object of the same type as the exception object, the copy operation
can be omitted by treating the exception-declaration as an alias for
the exception object if the meaning of the program will be unchanged
except for the execution of constructors and destructors for the
object declared by the exception-declaration."

Examples:
try {...}
catch(SomeError) {
 // copy can be elided because this doesn't change behavior
}

try {...}
catch(SomeError e) {
 e.virtualFunc(); // copy can't be elided because dynamic type of
exception object could be different
}

try {...}
catch(SomeError e) {
 e.modify();
 throw; // copy can't be elided because modification would be visible
at next handler
}

So far, so good. Paragraph 16 now goes on:

"When the criteria for elision of a copy operatoin are met and the
object to be copied is designated by an lvalue, overload resolution to
select the constructor for the copy is first performed as if the
object were designated by an rvalue."

Now consider the following example:

try {...}
catch(std::vector<int>) {
 throw;
}

In C++03, the copy could be elided, since it would have no effect.
Since the elision rules haven't been changed, it would be a fair
assumption that the copy can be elided in C++0x too. But this means
that if the compiler does not elide the copy, it must still do the
overload resolution in such a way that it chooses vector's move
constructor. But this means that the rethrown vector has been moved
from! Clearly this cannot be correct behavior.

Which part of the standard prevents such a defect?
Does the phrase "if the meaning of the program will be unchanged" from
paragraph 15 apply to changes that result from paragraph 16? If so,
this should be clarified by a note in p15, because the wording of p16
("When the criteria ...") implies that the decision whether the
criteria are met is entirely p15's business.

The phrase is underspecified anyway, now that program legality depends
on it. Take this example:

struct MyError {
 MyError() {}
 MyError(MyError&&); // out-of-line
 MyError(const MyError&) = delete;
};
try { throw MyError(); }
catch(MyError e) { throw; }

The above program is well-formed iff the compiler knows that the move
constructor does not modify the source. If it does, then it could
elide the copy, and thus performs overload resolution with an rvalue,
finding the move constructor. If the compiler doesn't know this, it
has to assume modification, must do overload resolution with an
lvalue, and decides on the deleted copy constructor - error.
A cross-module optimizing compiler might have such information. Most
compilers won't. But the spectrum of how much information a compiler
has is wide.

So I think this is a defect no matter how you turn it.


Possible solutions:
1) Define precisely under which circumstances the copy for a throw
block may be elided. (I recommend to allow it when the catch block
does not rethrow - although that too may be difficult to prove, since
the rethrow statement may be arbitrarily deeply hidden in function
calls - and the caught type is not polymorphic.)
2) Do not allow the copy into the exception object to be elided. I
don't think any compiler does this anyway.
3) Make this bullet an exception for p16, i.e. something like, "When
the crtieria for elision of a copy operation are met and the copy
operation does not initialize the object of an exception-declaration
and ..."

I think the best solution is #2, since it is the simplest and
completely sidesteps all the involved issues. Since copy elision is a
matter of optimization and not correctness, programs should not depend
on this behavior, and of those that do (possibly only in terms of
performance), I doubt there'll be a single one that depends on the
elision of the copy to the catch object.


Proposed resolution:
In 12.8p15, completely remove the fourth bullet ("when the exception-
declaration ...").

Sebastian

--
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html                      ]





Author: daniel.kruegler@googlemail.com
Date: Tue, 21 Apr 2009 16:19:07 CST
Raw View
On 2 Apr., 02:36, wasti.r...@gmx.net wrote:
> 12.8p15 of N2857, i.e. the most recent draft, specifies the
> circumstances under which copy elision is permitted. The fourth bullet
> says:
>
> "when the exception-declaration of an exception handler declares an
> object of the same type as the exception object, the copy operation
> can be omitted by treating the exception-declaration as an alias for
> the exception object if the meaning of the program will be unchanged
> except for the execution of constructors and destructors for the
> object declared by the exception-declaration."
>
> Examples:
> try {...}
> catch(SomeError) {
>  // copy can be elided because this doesn't change behavior
> }
>
> try {...}
> catch(SomeError e) {
>  e.virtualFunc(); // copy can't be elided because dynamic type of
> exception object could be different

Not precisely. Note that 12.8p15 kicks in, if the handler "declares
an
object of the same type (except for cv-qualification) as the
exception
object", so both must either have the same dynamic type for the
elision rule or otherwise the elision rule wouldn't be considered.

> }
>
> try {...}
> catch(SomeError e) {
>  e.modify();
>  throw; // copy can't be elided because modification would be visible
> at next handler

I agree.

> }
>
> So far, so good. Paragraph 16 now goes on:
>
> "When the criteria for elision of a copy operatoin are met and the
> object to be copied is designated by an lvalue, overload resolution to
> select the constructor for the copy is first performed as if the
> object were designated by an rvalue."
>
> Now consider the following example:
>
> try {...}
> catch(std::vector<int>) {
>  throw;
>
> }
>
> In C++03, the copy could be elided, since it would have no effect.
> Since the elision rules haven't been changed, it would be a fair
> assumption that the copy can be elided in C++0x too. But this means
> that if the compiler does not elide the copy, it must still do the
> overload resolution in such a way that it chooses vector's move
> constructor. But this means that the rethrown vector has been moved
> from! Clearly this cannot be correct behavior.

I think you misunderstood the wording here. The *first*
logic decision must be to consider [class.copy]/15
to check whether "an implementation is allowed to omit
the copy construction of a class object". As you point
out, the only relevant bullet is no. 4. But because your
exception handler evaluates a "throw;" expression
the mentioned elision would violate the part:

"if the meaning of the program will be unchanged"

This means that the conditions are *not* met and this
again means that p.16 does also *not* apply, because
it says:

"When the criteria for elision of a copy operation are met[..]"

This means that a normal copy construction must
take place here (any further magic insight of the compiler
excluded).

> Which part of the standard prevents such a defect?
> Does the phrase "if the meaning of the program will be unchanged" from
> paragraph 15 apply to changes that result from paragraph 16?

I think so.

> If so, this should be clarified by a note in p15, because the wording
> of p16 ("When the criteria ...") implies that the decision whether the
> criteria are met is entirely p15's business.

Hmmh, I'm not sure that I can follow you here. I also read it so that
p15 contains the complete logic decision tree that evaluates
whether elision of the *copy* may happen or not.

> The phrase is underspecified anyway, now that program legality depends
> on it. Take this example:
>
> struct MyError {
>  MyError() {}
>  MyError(MyError&&); // out-of-line
>  MyError(const MyError&) = delete;};
>
> try { throw MyError(); }
> catch(MyError e) { throw; }

MyError is not a feasible exception object, because
15.1 [except.throw] requires that the type is copy
constructible, see p.5:

"When the thrown object is a class object, the copy
constructor and the destructor shall be accessible, even
if the copy operation is elided"

> The above program is well-formed iff the compiler knows that the move
> constructor does not modify the source. If it does, then it could
> elide the copy, and thus performs overload resolution with an rvalue,
> finding the move constructor. If the compiler doesn't know this, it
> has to assume modification, must do overload resolution with an
> lvalue, and decides on the deleted copy constructor - error.

[This part depends on the original assumption that the
above class would be a feasible exception object type,
which it isn't]

> A cross-module optimizing compiler might have such information. Most
> compilers won't. But the spectrum of how much information a compiler
> has is wide.
>
> So I think this is a defect no matter how you turn it.
>
> Possible solutions:
> 1) Define precisely under which circumstances the copy for a throw
> block may be elided. (I recommend to allow it when the catch block
> does not rethrow - although that too may be difficult to prove, since
> the rethrow statement may be arbitrarily deeply hidden in function
> calls - and the caught type is not polymorphic.)
> 2) Do not allow the copy into the exception object to be elided. I
> don't think any compiler does this anyway.
> 3) Make this bullet an exception for p16, i.e. something like, "When
> the crtieria for elision of a copy operation are met and the copy
> operation does not initialize the object of an exception-declaration
> and ..."

For me this seems superfluous.

> I think the best solution is #2, since it is the simplest and
> completely sidesteps all the involved issues. Since copy elision is a
> matter of optimization and not correctness, programs should not depend
> on this behavior, and of those that do (possibly only in terms of
> performance), I doubt there'll be a single one that depends on the
> elision of the copy to the catch object.
>
> Proposed resolution:
> In 12.8p15, completely remove the fourth bullet ("when the exception-
> declaration ...").

I see no convincing reason why. Most of your example
handlers seem rather artifical to me. Many style-guides
forbid exception handler types which are reference types.
In cases where they are allowed, it seems not very
intuitive to decide for a by-value handler, if a "throw;"
expression is potentially evaluated.

Just my 2 Euro cents,

Daniel


--
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html                      ]





Author: daniel.kruegler@googlemail.com
Date: Wed, 22 Apr 2009 12:55:46 CST
Raw View
On 22 Apr., 00:19, daniel.krueg...@googlemail.com wrote:
[..]
> I see no convincing reason why. Most of your example
> handlers seem rather artifical to me. Many style-guides
> forbid exception handler types which are reference types.

Sorry, that should say: "which aren't references".

> In cases where they are allowed, it seems not very
> intuitive to decide for a by-value handler, if a "throw;"
> expression is potentially evaluated.

- Daniel

--
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html                      ]