Is there any special reason why the move constructor is not elided in the snippet shown below?
Asked Answered
S

1

14

gcc, clang and VS2015 don't elide the call to the move constructor in the code below, after throwing object a. It seems to me the conditions established in bullet point (31.2) of §8.12[class.copy]/31 (N4140) are satisfied.

#include <iostream>

struct A
{
    A() { std::cout << "Default ctor " << '\n'; }
    A(const A& a) { std::cout << "Copy ctor" << '\n'; }
    A(A&& a) { std::cout << "Move ctor" << '\n'; }
    ~A() { std::cout << "Destructor " << '\n'; }
};

int main()
{
    try
    {
        A a;
        throw a;
    }
    catch(A& a) { std::cout << "Caught" << '\n'; }
}

Note that a is an lvalue, but according to §12.8/32, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. That is, the call to the move constructor is OK. If you erase the definition of the move constructor above, the copy constructor is invoked, but again, it is not elided!

I understand the copy-elision is not mandated by the Standard, but I'm curious to know if there is any special condition that could justify the fact, that the three compilers mentioned above avoid this optimization, in this particular example.

An example output for gcc, from the link above:

g++ -std=c++14 -O2 -Wall -pedantic -pthread main.cpp && ./a.out

Default ctor

Move ctor

Destructor

Caught

Destructor

Snowman answered 27/9, 2015 at 19:37 Comment(12)
it's along the exceptional path, who cares? the equivalent happy path fully optimizesHegelian
I would think it's about wording that a local variable cannot be elided if its construction has side effects. Not sure if there's really a conflict but as simple rules they might be effectively conflicting for the compiler.Ankylosaur
@Cheersandhth.-Alf: Copy-elision is explicitly allowed so that the compiler can do it even is there are side effects.Evslin
@DietmarKühl: No, I'm not talking about the side effects of the move constructor.Ankylosaur
@DietmarKühl VS2015 always does copy-elision even when optimizations are disabled. I don't know the details about gcc and clang. But you can verify the compiler switches that were used in gcc, in the link provided above,Snowman
@DietmarKühl: No, I mean the side effects of the default constructor here. If there was no throwing the variable could not be elided, even though it's not used, because its construction has side effects. I guess one could test that possibility (rule conflict for the compiler) by removing the side effect.Ankylosaur
What actually is the output of the program? Just so everyone knows exactly what the sequence is from the text of the question.Crabwise
@DietmarKühl: well it doesn't matter if you understand what I meant, because that possibility didn't pan out. :(Ankylosaur
@AlanStokes You'll be able to see the output by clicking the link at the top of the question. I'll repeat it here: coliru.stacked-crooked.com/a/19d414d2bc28ba48Snowman
I imagine the answer has to do with how exception objects are stored, allocated, and deallocated. But I don't know enough details about that.Sinking
@aschepler: With respect to the optimizations the code is equivalent (roughly) to declaring a second variable b copy-initialized from a. However, as already discussed, removing the side effect from the default destructor does not elide a in this case, with g++. It could do that under the as-if rule.Ankylosaur
I did some research for clang and came up with this. Not enough for an answer but I hope that at least it makes sense.Upstroke
F
3

According to 12.8 [class.copy] paragraph 31, second bullet the copy of a local variable being thrown can be elided:

in a throw-expression, when the operand is the name of a non-volatile automatic object (other than a function or catch-clause parameter) whose scope does not extend beyond the end of the innermost enclosing try-block (if there is one), the copy/move operation from the operand to the exception object (15.1) can be omitted by constructing the automatic object directly into the exception object

It seems none of the compilers makes use of this optimization. One reason could be that it is simply not worth doing so far as the effort is better spent on other optimizations. I don't think there is anything in the standard prohibiting this optimization.

Finegan answered 27/9, 2015 at 20:26 Comment(4)
One practical reason for not doing it is that this can reorder side effects, where something that the local variable's construction does and that following statements before the throw depend on, is moved to the throwing.Ankylosaur
The relevant standardese for that is in C++11 §3.7.3, stating that the local object (and the side effects of its construction, that following statements may depend on) can be eliminated, "If a variable with automatic storage duration has initialization or a destructor with side effects, it shall not be destroyed before the end of its block, nor shall it be eliminated as an optimization even if it appears to be unused, except that a class object or its copy/move may be eliminated as specified in 12.8".Ankylosaur
Wow, I'm surprised there's a distinct provision for this case. Nice find.Ration
Defect report 887 shows that the current wording was adopted due to unsafety of the original. Since there's still extreme unsafety (reordering of side effects) this is arguably a defect in the standard. A simple resolution would be to simply ditch the bullet point, since moving can be done explicitly when it's desired. However I don't see any defect report directly about it.Ankylosaur

© 2022 - 2024 — McMap. All rights reserved.