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?
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 :)
© 2022 - 2024 — McMap. All rights reserved.