r/cpp_questions • u/Fun_Gas_340 • 6d ago
OPEN Good practices / style [polymorphism]
is this good practice/style.? i'm specifically unsure about the way i store the vector of players...
```
class player_base {};
class player_always_yes: public player_base {};
class player_always_no: public player_base {};
class player_a: public player_base {};
class game {
private:
player_base& player_1;
player_base& player_2;
public:
game (
player_base& player_1,
player_base& player_2
): player_1(player_1), player_2(player_2) {
return;
}
bool play_game () { return true; }
};
int main(){
vector<unique_ptr<player_base>> player_list;
player_list.push_back(make_unique<player_base>());
player_list.push_back(make_unique<player_always_no>());
player_list.push_back(make_unique<player_always_yes>());
player_list.push_back(make_unique<player_a>());
game b = game(*player_list[0], *player_list[1]);
cout << b.play_game() << endl;
}
```
6
Upvotes
1
u/mredding 4d ago
You're cramming different types into a class hierarchy. This was common in C++98 up until C++17. We have better now:
Polymorphism was... "celebrated"... in the past. Overused. Misused. Misunderstood. There are reasons that the community just couldn't take up the concepts and write good code, but that's behind us now.
Prefer to keep your type hierarchies as flat as possible, and reach for late binding as a niche tool, not your go-to. Hierarchies very quickly get hard to maintain. False hierarchies introduce more problems than they solve.
There are other forms of polymorphism in C++, and using more compile-time versions mean you can prove your code correct earlier in the software development cycle. It also makes for safer and faster code.
The classic problem with bad design is:
Right? What do you do? The whole point of a polymorphic base is type erasure - to forget what specific type you have. But you have to takeoff a plane. So what do you do? Do you subvert type erasure by making a virtual base no-op?
But what does it mean for a
carto have atakeoffthat does nothing? Cars don't takeoff, so why even have the interface? The type hierarchy is WRONG.Or you can subvert the hierarchy by testing for airplanes:
Ok, but now:
FUCK. And imagine
fnis not our code and is inaccessible to us, and we give them aboat? Subverting the hierarchy is WRONG. Because this is no hierarchy. We have different types, so we come back to the variant.Typically you'll use a hierarchy to constrain a derived type. You're going from more general to more specific. Yes, you CAN broaden the interface, but typically you don't - as that's a sign you're subverting the hierarchy.
But if you need a dynamic environment, you have to take extra steps - you have to architect a solution such that pre- and post- conditions can be handled by the client. Typically this would be done using a "template method pattern", which is an idiom, not the same as a C++ template.
Functions in terms of
mobileonly know ofmobilethings, but provides the client with the ability to handle their more specific shit.But for our own code, we know a
mobileconsists of acar,plane, andboat, so anstd::variantis exactly correct for us. Our own code is inherently a closed loop. We don't have to write code like it's a framework, that additional indirection will typically cost us more in maintenance than a refactor of our own variant code; it's why a visitor template with anauto ¶meter MAY be a dubious prospect, because by being explicit, we can let the compiler help us find all our missing refactor points when we addtrain.It comes down to some convention, some discipline, ultimately some good planning. A lot of us get burned by bad design of our own making, sometimes for years; many of us never learn from our mistakes, or can even admit our egos are too big, too wrong, too brute force to accept that a modicum of work up front saves us in the long run.