In C++ code I see two popular and somewhat incompatible ways of handling errors: exceptions and return codes. Discussions with colleagues on which one to use almost always boils down to this incompatibility. Everyone argues for the approach they've been using thus far, because it works with the code they've written so far. I believe, on its own, that is a weak argument, especially since bridgen the two appraoches is far from impossible.
Occasionally, I've also seen colleagues argue for some parts of one approach, without using the others. I argue that the two approaches are a complete package. You either use all of it, or nothing.
struct thing_t {
thing_t() {
// May throw.
}
bool is_admin() {
// May return false.
return true;
}
void do() {
// May throw.
throw std::runtime_error("Error");
}
};
void do_thing() {
auto thing = std::make_unique<thing_t>();
if (!thing->is_admin()) {
return;
}
thing->do();
}
Pros:
std
.Cons:
struct thing_t {
bool do() {
// May fail.
return false;
}
bool is_admin() {
// May return false.
return true;
}
};
thing_t *make_thing() {
// May fail and return nullptr.
return new thing_t();
}
bool do_thing() {
thing_t *thing = make_thing();
bool error = false;
if (!thing) {
error = true;
}
if (!error && !thing->is_admin()) {
error = true;
}
if (!error) {
error = thing->do();
}
delete thing;
return error;
}
Pros:
Cons:
Note that I repeatedly check error
. An alternative version I often see is to nest the branches. I also see colleagues try to use early returns in code like this, requiring the clean-up code to be repeated before each return. I am not a fan of either.
I am aware that this code behaves slightly different, as when is_admin
returns false
, it errors, whereas the other example doesn't. Usually I would see code where error
is an enumerate type containing some non-error values for this case.
As mentioned in the intro, care must be taken when mixing the two approaches. Individual functions should only use one approach, and calls to functions using the other approach need to be wrapped.
struct thing_t {
bool do() {
// May fail.
return false;
}
bool is_admin() {
// May return false.
return true;
}
};
thing_t *make_thing() {
// May fail and return nullptr.
return new thing_t();
}
void destroy_thing(thing_t *thing) {
delete thing;
}
struct thing_deleter_t {
void operator()(thing_t *thing) {
destroy_thing(thing);
}
};
void check(bool error) {
if (error) {
throw std::runtime_error("Error");
}
}
void do_thing() {
std::unique_ptr<thing_t, thing_deleter_t> thing(make_thing());
check(!thing);
if (!thing->is_admin()) {
return;
}
check(thing->do());
}
In personal projects I write code like this when interfacing with C APIs.
I want to write an example using std::out_ptr
.
struct thing_t {
thing_t() {
// May throw.
}
bool is_admin() noexcept {
// May return false.
return true;
}
bool do() {
// May throw.
throw std::runtime_error("Error");
}
};
bool do_thing() {
bool error = false;
thing_t *thing = nullptr;
try { thing = new thing_t(); } catch (...) { error = true; }
if (!thing->is_admin()) {
error = true;
}
if (!error) {
try { error = thing->do(); } catch (...) { error = true; }
}
delete thing;
return error;
}