Setup and teardown routines in non-flat class hierarchies
Here’s a C++ problem where I still don’t know what’s a good way to deal with it.
(But I asked on Slack the other day and got some good-sounding advice from Kevin Zheng.
See further down in this post.)
Consider a class hierarchy something like this. We have an abstract base class Garment,
a derived class Shirt, and a leaf class DressShirt that derives from Shirt.
To don a DressShirt means to first do everything you’d do to don the Shirt part of
the object, and then additionally apply the cufflinks.
class Garment {
public:
virtual void don() = 0;
};
class Shirt : public Garment {
Sleeves sleeves_;
public:
void don() override {
sleeves_.insert_arms();
}
};
class DressShirt final : public Shirt {
Cufflinks cufflinks_;
public:
void don() override {
this->Shirt::don();
cufflinks_.apply();
}
};
This way of organizing things does work, but it fails to conform to the Non-Virtual Interface idiom. (Previously on this blog: “A C++ acronym glossary” (2019-08-02), “CppCon 2019 talks are up” (2019-10-18).) The NVI version would look more like this (Godbolt):
class Garment {
virtual void do_don() = 0;
public:
void don() { this->do_don(); }
};
class Shirt : public Garment {
Sleeves sleeves_;
void do_don() override {
sleeves_.insert_arms();
}
};
class DressShirt final : public Shirt {
Cufflinks cufflinks_;
void do_don() override {
this->Shirt::don(); // OOPS!
cufflinks_.apply();
}
};
…Except that this produces an infinite loop!
The NVI idiom prevents anyone from deliberately calling a “sliced” version of
any virtual function via qualified call syntax. You can’t say myDressShirt.Shirt::do_don()
because do_don() is private at every level. However, the NVI does not protect us
against someone trying to make a qualified call and accidentally getting themselves
into trouble!
In the above code, this->Shirt::don() is a silly (but valid) way to write this->don();
which (because we’re using the NVI idiom) means this->do_don(); which means
infinite recursion.
To express the statement that “to don a dress shirt, you first don the shirt part and then apply the cufflinks,” we would have to refactor our class hierarchy in some way. For example:
class Garment {
virtual void do_don() = 0;
public:
void don() { this->do_don(); }
};
class Shirt : public Garment {
Sleeves sleeves_;
protected: // OK but yuck!
void do_don() override {
sleeves_.insert_arms();
}
};
class DressShirt final : public Shirt {
Cufflinks cufflinks_;
void do_don() override {
this->Shirt::do_don(); // OK
cufflinks_.apply();
}
};
However, now we’re using protected (which personally I consider a code smell,
by the way), and we’ve cracked open our nice safe NVI don method by calling
Shirt::do_don with qualified syntax — exactly the thing we were using NVI
to get away from!
Kevin Zheng suggested (something isomorphic to) the following design:
class Garment {
virtual void do_don() = 0;
public:
void don() { this->do_don(); }
};
class Shirt : public Garment {
Sleeves sleeves_;
void do_don() override {
this->shirt_specific_don();
}
protected:
void shirt_specific_don() {
sleeves_.insert_arms();
}
};
class DressShirt final : public Shirt {
Cufflinks cufflinks_;
void do_don() override {
this->shirt_specific_don();
cufflinks_.apply();
}
};
This design has the same essential shape as the previous one, except
that we are never using the verboten qualified call syntax. Instead of
this->Shirt::do_don(), we call this->shirt_specific_don(), whose name
clearly indicates its purpose.
…Ish. If there’s some additional code required to don a Garment, then
it’s unclear whether shirt_specific_don() should itself call
garment_specific_don(), or whether DressShirt::do_don() should call
both garment_specific_don() and shirt_specific_don() in that order.
Okay, since
Garmentis the root class, we could just put ourGarment-specific code directly into the non-virtualGarment::donmethod. But let’s ignore that possibility, because it wouldn’t apply to aClothGarmentclass introduced betweenGarmentandShirt.
And we still haven’t gotten rid of the requirement for DressShirt to
know about protected details of Shirt. In other words, DressShirt
must know more about Shirt than you’d normally expect to need to
know — more than its public API. Ideally, I’d like to be able to inherit
from Shirt just as easily as I can compose with it.
But maybe this is just another argument in favor of “Prefer composition over inheritance.” If we use composition, then we have something more like this:
class Garment {
virtual void do_don() = 0;
public:
void don() { this->do_don(); }
};
class Shirt final : public Garment {
Sleeves sleeves_;
void do_don() override {
sleeves_.insert_arms();
}
};
class DressShirt final : public Garment {
Shirt shirt_;
Cufflinks cufflinks_;
void do_don() override {
shirt_.don();
cufflinks_.apply();
}
};
That is, we can avoid this problem related to non-flat class hierarchies by… flattening our class hierarchy!
Is this really the only elegant way out of our difficulty?
Am I missing some idiom or pattern that would make our original
DressShirt less wonky? If you know a good answer not mentioned
here, please let me know!
Update (2019-12-12): Benjamin Kaufmann writes in with an alternative solution.
His solution stems from Scott Meyers’ advice (More Effective C++, item 33):
“Make non-leaf classes abstract.” Since Shirt is a non-leaf class, we should make it abstract,
which means creating the concrete class PlainShirt as a new leaf off of Shirt.
class Garment {
virtual void do_don() = 0;
public:
void don() { this->do_don(); }
};
class Shirt : public Garment {
Sleeves sleeves_;
void do_don() final {
sleeves_.insert_arms();
this->do_subshirt_specific_don();
}
virtual void do_subshirt_specific_don() = 0;
};
class PlainShirt final : public Shirt {
void do_subshirt_specific_don() final {}
};
class DressShirt final : public Shirt {
Cufflinks cufflinks_;
void do_subshirt_specific_don() final {
cufflinks_.apply();
}
};
This version inverts Kevin Zheng’s approach. In Kevin’s version, we had to hope that the child class
would (A) override do_don correctly and (B) remember to call shirt_specific_don in its
implementation of do_don. In Ben’s version, we solve (A) by introducing a new pure virtual function
do_subshirt_specific_don which all children of Shirt must override. We solve (B) by calling
the Shirt-specific code sleeves_.insert_arms() as part of Shirt::do_don and by marking
Shirt::do_don as final so that no ill-behaved child class can mess with it. Children of Shirt
cannot mess with the implementation of Garment::don, nor with the implementation of Shirt::do_don;
they are allowed only to implement do_subshirt_specific_don, and in fact they must implement
do_subshirt_specific_don — even if it’s a no-op, as in class PlainShirt.
I like Ben’s solution very much!
