50 Intermediate The Hundred
The Dog That Lost Its Woof
the shelter that takes in dogs and hands back something quieter
#include <iostream>
#include <string>
#include <vector>
struct Animal {
virtual std::string speak() const { return "..."; }
virtual ~Animal() = default;
};
struct Dog : Animal {
std::string speak() const override { return "woof!"; }
};
int main() {
Dog rex;
std::cout << rex.speak() << "\n"; // straight from the dog's mouth
std::vector<Animal> shelter;
shelter.push_back(rex); // check rex into the shelter
std::cout << shelter[0].speak() << "\n";
}
Run it. What does the shelter's dog say?
Answer
woof!, then ... — the vector's copy of rex is a plain Animal; the Dog part
never made it in.
Why¶
A std::vector<Animal> stores Animal objects by value — every slot holds exactly one
Animal, no more. push_back(rex) therefore copy-constructs an Animal from just the
Animal subobject of rex; everything Dog-specific, the override included, is
sliced away. Virtual dispatch still works perfectly on the element — it is a genuine,
complete Animal, so speak() correctly finds Animal::speak. Slicing is entirely
legal: no cast, and GCC's -Wall -Wextra say nothing. The same slice happens with
Animal a = rex; and with functions taking Animal by value. clang-tidy's
cppcoreguidelines-slicing check flags the direct copy ("discards override 'speak'") —
but not the push_back, where the copy hides inside vector's machinery.
The fix¶
Store the animals through a layer of indirection — a pointer can point at the whole dog:
std::vector<std::unique_ptr<Animal>> shelter;
shelter.push_back(std::make_unique<Dog>());
std::cout << shelter[0]->speak() << "\n"; // woof!
(References and std::variant also preserve the derived part — anything but a base-class
value.)
Takeaway: virtual dispatch travels only through pointers and references — copy a derived object into a base value and you keep the base, lose the rest.
Try it: g++ -std=c++17 main.cpp -o demo && ./demo