More Usable Code By Avoiding Two Step Objects

header image

Two step initialization is harmful to the objects that you write because it obfuscates the dependencies of the object, and makes the object harder to use.

Harder to use

Consider a header and some usage code:

struct Monkey
{
    Monkey();
    void set_banana(std::shared_ptr const& banana);
    void munch_banana();
private:
    std::shared_ptr const& banana;
};

int main(int argc, char** argv)
{
    Monkey jim;
    jim.munch_banana();
    ...
}

Now jim.munch_banana(); could be a valid line to call, but the reader of the interface isn’t really assured that it is if the writer wrote the object with two step initialization. If the implementation is:

Monkey::Monkey() :
    banana{nullptr}
{
}
void Monkey::set_banana(std::shared_ptr const& b)
{
    banana = b;
}
void Monkey::munch_banana()
{
    banana->decrement_weight();
}

Then calling jim.munch_banana(); would segfault! A more careful coder might have written:

void Monkey::munch_banana()
{
    if (banana)
        banana->decrement_weight();
}

This still is a problem though, as calling munch_banana() is silently doing nothing; and the caller can’t know that. If you tried to fix by writing:

void Monkey::munch_banana()
{
    if (banana)
        banana->decrement_weight();
    else
        throw std::logic_error("monkey doesn't have a banana");
}

We’re at least to the point where we haven’t segfaulted and we’ve notified the caller that something has gone wrong…. But we’re still at the point where we’ve thrown an exception that the user has to recover from.

Obfuscated Dependencies

With the two-step object, you need more lines of code to initialize it, and you leave the object “vulnerable”.

auto monkey = std::make_unique();
monkey->set_banana(std::make_shared());

If you notice, between lines 1 and 2, monkey isn’t really a constructed object. It’s in an indeterminate state! If monkey has to be passed around to an object that has a Banana to share, thats a recipe for a problem. Other objects don’t have a good way to know if this is a Monkey object, or if its a meta-Monkey object that can’t be used yet.

Can we do better?

Yes! By thinking about our object’s dependencies, we can avoid the situation altogether.
The truth is; Monkey really does depend on Banana..
If the class expresses this in its constructor, ala:

struct Monkey
{
    Monkey(std::shared_ptr const& banana);
    void set_banana(std::shared_ptr const& banana);
    void munch_banana();
private:
    std::shared_ptr banana;
};

We make it clear when constructing that the Monkey needs a Banana. The coder interested in calling Monkey::munch_banana() is guaranteed that it’ll work. The code implementing Monkey::munch_banana() becomes the original, and simple:

void Monkey::munch_banana()
{
    banana->decrement_weight();
}

Furthermore, if we update the banana later via Monkey::set_banana(), we’re still in the clear. The only way the coder’s going to run into problems is if they explicitly set a nullptr as the argument, which is a pretty easy error to avoid, as you have to actively do something silly, instead of doing something reasonable, and getting a silly error.

Getting the dependencies of the object right sorts out a lot of interface problems and makes the object easier to use.

This entry was posted in Coding. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *