Why doesn't std::exception have a move constructor?
Asked Answered
M

2

6

The best practice regarding exceptions in C++ seems to be throw by value, catch by reference. I had a look at <exception> and cppreference and I see that std::exception has a copy-constructor but no move-constructor, why is this? Wouldn't having a move-constructor allow cheaply catching by value and thus simplifying the guidelines?

Malkin answered 15/7, 2020 at 6:35 Comment(1)
Since catching by value would cause slicing, and make exception type hierarchies pointless, I don't see the guidelines changing.Simpleminded
W
3

The descendants of std::exception do own data. For example std::runtime_error owns its what() message. And that message is dynamically allocated because it can be an arbitrarily long message.

However, the copy constructor is marked noexcept (implicitly) because the std::exception copy constructor is noexcept.

#include <stdexcept>
#include <type_traits>

int
main()
{
    static_assert(std::is_nothrow_copy_constructible<std::runtime_error>{});
}

The only way for a class to own a dynamically allocated message, and have a noexcept copy constructor, is for that ownership to be shared (reference counted). So std::runtime_error is essentially a const, reference counted string.

There was simply no motivation to give these types a move constructor because the copy constructor is not only already very fast, but the exceptional path of program is only executed in exceptional circumstances. About the only thing a move constructor for std::runtime_error could do is eliminate an atomic increment/decrement. And no one cared.

Wouldn't having a move-constructor allow cheaply catching by value and thus simplifying the guidelines?

You can already cheaply catch by value. But the guideline exists because exceptions are often part of an inheritance hierarchy, and catching by value would slice the exception:

#include <exception>
#include <iostream>
#include <stdexcept>

int
main()
{
    try
    {
        throw std::runtime_error("my message");
    }
    catch (std::exception e)
    {
        std::cout << e.what() << '\n';
    }
}

Output (for me):

std::exception
Winonawinonah answered 15/7, 2020 at 13:25 Comment(0)
M
1

I checked on a couple of compilers and apparently the try/catch mechanism doesn't use move semantics.

#include <iostream>
using namespace std;

struct err {
    err() { cout << "created err" << endl; }
    err(const err&) { cout << "copied err" << endl; } // Compilation error if this is commented out
    err(err&&) noexcept { cout << "moved err" << endl; }
    ~err() { cout << "destroyed err" << endl; }
};

void foo() {
    throw err{};
}

int main() {
    try {
        cout << "calling foo" << endl;
        foo();
        cout << "called foo" << endl;
    }
    catch (err e) {
        cout << "caught err" << endl;
    }
}

Output:

calling foo
created err
copied err
caught err
destroyed err
destroyed err

So having a move constructor would be meaningless.

Why this is so is probably a matter for another question :)

Malkin answered 15/7, 2020 at 8:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.