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
Garment
is the root class, we could just put ourGarment
-specific code directly into the non-virtualGarment::don
method. But let’s ignore that possibility, because it wouldn’t apply to aClothGarment
class introduced betweenGarment
andShirt
.
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!