Almost all the suggestions I see seem to leave kind of the same problem.
Whether init
returns a bool
, or you have a bool
member named ok_
, or a factory that returns a null pointer, the code that tries to use the object has to follow pretty much the same pattern:
Try to create an object
Check whether that succeeded
if it succeeded
do the normal stuff
else
handle the failure
...and pretty much everywhere you try to use the object, you have similar code:
if we have a valid foo
use the valid foo
else
react to not having a valid foo
Whether we encode the failure in a member variable ok_
or in a Boolean return value or a null pointer as a return value, we still end up with pretty much the same basic situation: we've signaled a failure, and now it's up to the calling code to figure out what to handle that failure. None of them makes much real difference in how we write the code.
The factory does (or at least can) help in one specific area though: we can add some logic there to (for example) log the error immediately if initialization fails. But I'd like to go a step further, so all the other places we might use the object have their error handling centralized as well.
As you might guess from the long-winded buildup, I think there's a fairly effective way to do that. One implementation would look something like this:
// ABC purely to define the interface:
class Foo {
public:
virtual bool init() = 0;
virtual void Bar() = 0;
virtual void Baz() = 0;
};
// This is the real implementation of the `Foo` class. It's pretty much
// like your existing code, but with the ctor make private, and
// the factory added as a friend.
class GoodFoo : public Foo {
GoodFoo();
friend Foo *makeFoo();
public:
virtual bool init() override { /* same as you've used before */ }
virtual void Bar() override { /* implementation of Bar */ }
virtual void Baz() override { /* implementation of Baz */ }
};
// This implementation handles what to do when we would normally call
// a member function on Foo, but initializing the Foo failed.
// As a simple implementation, we're just going to log the error.
class FailedFoo : public Foo {
FailedFoo();
friend Foo *makeFoo();
public:
// initialization always succeeds:
virtual bool init() override { return true; }
virtual void Bar() override {
// do whatever we would when we were going to call `Foo::Bar()`,
// but initializing the `Foo` failed
LOG(ERROR) << "Foo::Bar() called after init failed";
}
virtual void Baz() override {
// Likewise: for `Baz()` when initializing `Foo` failed
LOG(ERROR) << "Foo::Baz() called after init failed";
}
};
std::unique_ptr<Foo> makeFoo() {
auto ret = std::make_unique<GoodFoo>();
if (ret -> init())
return ret;
LOG(ERROR) << "Unable to initialize foo";
return std::make_unique<FailedFoo>();
}
Now we write the using code to kind of act like creating and initializing a Foo
always works:
auto foo = makeFoo(); // either works or logs the error
foo->Bar(); // again, either works, or logs the error
Of course, depending on the situation, Bar
or Baz
might return something to indicate success or failure--and obviously, the FailedFoo
implementation would always signal failure.