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?
std::move is actually just a request to move and if the type of the object has not a move constructor/assign-operator defined or generated the move operation will fall back to a copy.
No move constructor is automatically generated.
A move constructor allows the resources owned by an rvalue object to be moved into an lvalue without creating its copy. An rvalue is an expression that does not have any memory address, and an lvalue is an expression with a memory address.
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
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 :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With