Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Throwing movable objects

I've noticed a slight discrepency in how MSVC and g++ handle creation of the temporary exception object when the thrown type is movable. Hunting these down raised additional questions.

Before going any further, here is the core of my question: in the absense of copy/move elision, who well does the standard say how the temporary exception object should be created? At the moment, the best I've been able to do is the following quote, from 15.1/3:

A throw-expression initializes a temporary object, called the exception object, the type of which is determined by removing any top-level cv-qualifiers from the static type of the operand of throw and adjusting the type from “array of T” or “function returning T” to “pointer to T” or “pointer to function returning T”, respectively.

I'm guessing the answer is buried somewhere in language elsewhere that defines the type of an expression and how objects are initialized, but I'm having no luck piecing it all together. When an object is thrown, does the exception object get (a) copy constructed, (b) move constructed if appropriate, and copy constructed otherwise, or (c) initialized in an implementation defined manner?

Consider the following code:

#include <iostream>
using std::cout;
using std::cin;
using std::endl;

struct Blob {
  Blob() { cout << "C" << endl; }
  Blob(const Blob&) { cout << "c" << endl; }
  Blob(Blob&&) { cout << "m" << endl; }
  Blob& operator =(const Blob&) { cout << "=" << endl; return *this; }
  Blob& operator =(Blob&&) { cout << "=m" << endl; return *this; }
  ~Blob() { cout << "~" << endl; }

  int i;
};

int main() {
  try {
     cout << "Throw directly: " << endl;
     throw Blob();
  } catch(const Blob& e) { cout << "caught: " << &e << endl; }
  try {
     cout << "Throw with object about to die anyhow" << endl;
     Blob b;
     throw b;
  } catch(const Blob& e) { cout << "caught: " << &e << endl;  }
  {
    cout << "Throw with object not about to die anyhow (enter non-zero integer)" << endl;
    Blob b;
    int tmp;
    cin >> tmp; //Just trying to keep optimizers from removing dead code
    try {
      if(tmp) throw b;
      cout << "Test is worthless if you enter '0' silly" << endl;
    } catch(const Blob& e) { cout << "caught: " << &e << endl;  }
    b.i = tmp;
    cout << b.i << endl;
  }
}

This is all recreated on ideone. As you can [hopefully] see, gcc via ideone creates the Blob object in place in the first case, and moves in the second two. The results are summarized below, with the pointer values replaced with identifiers.

Throw directly: 
C {A}
caught: {A}
~ {A}
Throw with object about to die anyhow
C {A}
m {B} <- {A}
~ {A}
caught: {B}
~ {B}
Throw with object not about to die anyhow (enter non-zero integer)
C {A}
m {B} <- {A}
caught: {B}
~ {B}
2
~ {A}

The same code in MSVC2010, regardless of optimization settings, the results are the same except the two moves are copies. This is the difference that initially caught my eye.

The first test I assume is fine; its classic copy elision.

In the second test, gcc behaves the way I would have expected. The temporary Blob is treated as an xvalue, and the exception object is move constructed from it. But I'm not sure if the compiler is required to recognize that the original Blob is expiring; if it isn't it, then MSVC is acting correctly when it copies. Thus my original question: does the standard mandate what happens here, or is it just part of the implementation defined behavior inherant to exception handling?

The third test is exactly the opposite: MSVC behaves the way my intuition demands. gcc choses to move from b, but b is still live, as evidenced by the fact that I continue to work with it after handling the thrown exception. Obviously, in this trivial example, moving or copying makes no difference to b itself, but surely the compiler isn't allowed to look at that when considering overload resolution.

Obviously, the presence of copy/move elision makes this simple test hard to generalize from, but the bigger issue is that either compiler might not be complient just yet [particularly in gcc's case with the third test, and MSVC in general].

Note this is entirely for academic purposes; I almost never throw anything except a temporary, which both compilers construct in place anyhow, and I'm quite certain that behavior is allowed.

like image 639
Dennis Zickefoose Avatar asked Jun 15 '11 02:06

Dennis Zickefoose


2 Answers

The move behavior is conforming for case 2, but not case 3. See 12.8 [class.copy]/p31:

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or destructor for the object have side effects. ...

...

  • 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

The above doesn't define when an object can be implicitly moved. But it does define when copy/move elision is legal. To get when implicit move is legal you have to go down to paragraph 32 in the same section:

32 When the criteria for elision of a copy operation are met or would be met save for the fact that the source object is a function parameter, and the object to be copied is designated by an lvalue, overload resolution ...

This paragraph explains that when copy/move elision is legal, then overload resolution happens with two passes:

  1. First pretend the lvalue is an rvalue in deciding what constructor is going to be called or elided.

  2. If 1) fails, then repeat overload resolution with the argument as an lvalue.

This has the effect of producing a hierarchy of move semantics from best to worst:

  1. If you can elide construction, do so.
  2. Else if you can move the object out, do so.
  3. Else if you can copy the object out, do so.
  4. Else emit a diagnostic.

Note that these are essentially the same rules for the ordinary return of local stack objects.

like image 139
Howard Hinnant Avatar answered Nov 04 '22 06:11

Howard Hinnant


Throwing is a very implementation-defined behaviour. In C++03 then the exception was copied an implementation-defined amount of times, placed in an implementation-dependent place, referred to during the catch block and then destructed. In C++0x I expect that an implementation will either have the right to both copy and move it as many times as it likes, or move it as many times as it likes (i.e., you can throw non-copyable classes).

However, it's certainly not allowed that you could access an object that has been moved from during catch, as that would be Really Bad™. If you have done so, then it is a compiler bug. You should print the address of the object to be certain.

What you should also remember is that MSVC's implementation is to rules that existed many years ago, whereas GCC's rvalues implementation is much more recent. The rules may have changed since MSVC implemented theirs. However, the compiler will error on trying to throw a non-copyable class, suggesting to me that a compiler may both copy and move an exception object freely.

like image 42
Puppy Avatar answered Nov 04 '22 06:11

Puppy