When throwing an object, is it copied or moved?

Viewed 93

As far as I know, thrown objects are copied by default. So the copy constructor should be called when I throw an object. I also know that the compiler may optimize and elide the copying. I have a simple program here where I'm throwing a class called X. It's expected that if I make the class non-copyable, the object can't be thrown. But something unexpected happens. If I delete the copy constructor, the compiler complains about that. If I comment the deletion of the copy constructor and delete the move constructor, the complier complains about deleting the move constructor. the code:

class X
{
public:
    int code;
    X(int code) : code(code) {}
    //X(X&) = delete;
    //X(X&&) = delete;
};

void func()
{
    X x(4);
    throw x;
}

int main() { }

The error when I delete the copy constructor:

main.cpp: In function ‘void func()’:
main.cpp:13:11: error: use of deleted function ‘X::X(X&)’
     throw x;
           ^

The error when I delete the move constructor:

main.cpp: In function ‘void func()’:
main.cpp:13:11: error: use of deleted function ‘X::X(X&&)’
     throw x;
           ^

Can someone explain why this is happening?

Edit: If I delete the copy constructor and provide an implementation for the move constructor, the code works fine. Whereas if I delete the move constructor and provide an Implementation for the copy constructor, I still get the same error. Why can't I delete the move constructor?

code:

class X
{
public:
    int code;
    X(int code) : code(code) {}
    X(X&) = default;
    X(X&&) = delete;
};

void func()
{
    X x(5);
    throw x;
}

int main() { }

error:

main.cpp: In function ‘void func()’:
main.cpp:13:11: error: use of deleted function ‘X::X(X&&)’
     throw x;
           ^
3 Answers

The initialization of the exception object follows the same rules as the initialization of other objects: when the initializer is an lvalue, the copy constructor is used, and when the initializer is an rvalue, the move constructor is used. However, see C++17 [class.copy.elision]/3:

... if the operand of a throw-expression (8.17) 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), overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If the first overload resolution fails or was not performed, or if the type of the first parameter of the selected constructor is not an rvalue reference to the object’s type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue. [ Note: This two-stage overload resolution must be performed regardless of whether copy elision will occur. It determines the constructor to be called if elision is not performed, and the selected constructor must be accessible even if the call is elided. — end note ]

Here, you are throwing the object in a context where it is treated as an rvalue first. If overload resolution fails, it will then be treated as an lvalue. Therefore, in general, the compiler prefers to use the move constructor.

  1. When the copy constructor is explicitly deleted, the compiler does not generate the move constructor ([class.copy.ctor]/8), so the code doesn't compile.

  2. When the move constructor is explicitly deleted, the first stage of overload resolution selects the move constructor. An error occurs because the function selected by overload resolution is deleted. (Note: a defaulted move constructor that is defined as deleted is ignored by overload resolution ([class.copy.ctor]/10). An explicitly deleted move constructor is not ignored by overload resolution.)

  3. When both the copy and move constructors are explicitly deleted, the previous paragraph still holds.

When throwing an object, is it copied or moved?

The thrown object will be copy-initialised, and the lvalue local object will be treated as an rvalue - same as if returning a local lvalue.

Thus, if the type has a move constructor, then it will be moved in your example. If the class is only copyable but not movable, then it will be copied. Likewise, if the throw expression is not (treated as) an rvalue, then it is copied.

If the type is neither copyable nor movable, then it cannot be thrown. The copy or move may be elided in some cases but that has no effect on whether the type has to be copyable or movable.

Quoting from cppreference

Even if copy initialization selects the move constructor, copy initialization from lvalue must be well-formed, and the destructor must be accessible (since C++14)

And

If the type of expression is a class type, its copy/move constructor and destructor must be accessible even if copy elision takes place.

Apparently, for well defined exception class a copy constructor is necessary, even if it wouldn't be used. C++11 standard didn't mandate that, but it was fixed in C++14.

I'm not 100% sure how to interpret "This may call the move constructor for rvalue expression" - whether move constructor is also required or not. For backwards compatibility it shouldn't be, but perhaps someone can quote standard on that.

Related