Virtuální funkce, vtables a polymorfismus
Ponořte se do běhového polymorfismu: jak funguje virtuální odesílání pod pokličkou, kdy použít přepisovací a finální, abstraktní třídy a RTTI.
Polymorfismus za běhu
Polymorfismus znamená „mnoho forem“ — stejné rozhraní se chová odlišně v závislosti na skutečném typu objektu. V C++ je běhového polymorfismu dosaženo prostřednictvím virtuálních funkcí. Když zavoláte virtuální funkci prostřednictvím základního ukazatele nebo odkazu, je volání odesláno do skutečné implementace odvozené třídy za běhu.
Toto je mechanismus, díky kterému je OOP výkonný: můžete psát kód, který funguje na rozhraní základní třídy, a ten automaticky funguje správně s jakoukoli odvozenou třídou – dokonce i s těmi, které jsou napsány po vašem kódu.
Jak fungují vtables
Když má třída virtuální funkce, kompilátor vytvoří vtable (tabulku virtuálních funkcí) — pole ukazatelů funkcí. Každý objekt této třídy obsahuje skrytý vptr (ukazatel na vtable). Když je volána virtuální funkce, program následuje vptr do vtable, vyhledá ukazatel funkce a volá přes něj. Tato nepřímost je důvodem, proč mají virtuální hovory ve srovnání s přímými hovory malou režii.
#include <iostream>
#include <memory>
#include <vector>
class Shape {
public:
virtual ~Shape() = default;
// Pure virtual — no default implementation
virtual double area() const = 0;
// Virtual with default implementation
virtual std::string name() const { return "Shape"; }
};
class Circle : public Shape {
double radius_;
public:
explicit Circle(double r) : radius_{r} {}
double area() const override {
return 3.14159265 * radius_ * radius_;
}
std::string name() const override { return "Circle"; }
};
class Rectangle : public Shape {
double w_, h_;
public:
Rectangle(double w, double h) : w_{w}, h_{h} {}
double area() const override { return w_ * h_; }
std::string name() const override { return "Rectangle"; }
};
// Works with ANY Shape — past, present, or future
void print_info(const Shape& s) {
std::cout << s.name() << ": area = " << s.area() << '\n';
}
int main() {
Circle c{5.0};
Rectangle r{3.0, 4.0};
print_info(c); // Circle: area = 78.5398
print_info(r); // Rectangle: area = 12
// Polymorphic container
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(2.0));
shapes.push_back(std::make_unique<Rectangle>(6.0, 3.0));
for (const auto& s : shapes) {
print_info(*s); // virtual dispatch at runtime
}
return 0;
}Virtuální destruktory jsou povinné
Pokud má třída jakou virtuální funkci, její destruktor musí být virtuální. Bez něj způsobí odstranění odvozeného objektu prostřednictvím základního ukazatele nedefinované chování – odvozený destruktor se nikdy nespustí a unikne prostředky.
#include <iostream>
#include <memory>
class Base {
public:
// Without virtual destructor: UNDEFINED BEHAVIOR on delete
// virtual ~Base() = default; // ALWAYS do this!
~Base() { std::cout << "~Base\n"; }
};
class Derived : public Base {
int* data_;
public:
Derived() : data_{new int[1000]} {
std::cout << "Derived: allocated\n";
}
~Derived() {
delete[] data_;
std::cout << "~Derived: freed\n";
}
};
int main() {
Base* ptr = new Derived{};
delete ptr; // Only ~Base runs! ~Derived is NOT called!
// Result: memory leak (data_ never freed)
// Fix: make ~Base() virtual
return 0;
}přepsat a konečná klíčová slova
Klíčové slovo override říká kompilátoru, že hodláte přepsat virtuální funkci základní třídy. Pokud se podpis neshoduje (běžný překlep), kompilátor jej zachytí. Klíčové slovo final zabraňuje dalšímu přepisování funkce nebo brání dědění z třídy.
#include <iostream>
class Base {
public:
virtual ~Base() = default;
virtual void process(int x) const { std::cout << "Base: " << x << '\n'; }
virtual void execute() { std::cout << "Base::execute\n"; }
};
class Middle : public Base {
public:
// override: compiler checks signature matches Base::process
void process(int x) const override {
std::cout << "Middle: " << x * 2 << '\n';
}
// final: no further derived class can override execute()
void execute() final { std::cout << "Middle::execute\n"; }
};
class Bottom : public Middle {
public:
void process(int x) const override {
std::cout << "Bottom: " << x * 3 << '\n';
}
// ERROR if uncommented: execute() is final in Middle
// void execute() override { }
};
// A final class cannot be inherited from at all
class Leaf final : public Base {
public:
void process(int x) const override {
std::cout << "Leaf: " << x << '\n';
}
};
// ERROR if uncommented: cannot inherit from final class
// class Attempt : public Leaf {};
int main() {
Bottom b;
Base& ref = b;
ref.process(10); // Bottom: 30 (virtual dispatch)
ref.execute(); // Middle::execute (final version)
return 0;
}Abstraktní třídy a vzor NVI
Třída s alespoň jednou čistě virtuální funkcí (= 0) je abstraktní — nelze ji vytvořit. Vzor Non-Virtual Interface (NVI) poskytuje veřejný nevirtuální vstupní bod, který volá soukromou virtuální funkci a umožňuje základní třídě řídit podmínky před/po.
#include <iostream>
#include <string>
// NVI Pattern: public non-virtual calls private virtual
class Validator {
public:
virtual ~Validator() = default;
// Public non-virtual interface — controls the workflow
bool validate(const std::string& input) const {
if (input.empty()) {
std::cout << "[Validator] Input is empty\n";
return false;
}
// Delegate to derived class implementation
bool result = do_validate(input);
std::cout << "[Validator] " << (result ? "PASS" : "FAIL") << '\n';
return result;
}
private:
// Pure virtual — derived classes MUST implement
virtual bool do_validate(const std::string& input) const = 0;
};
class EmailValidator : public Validator {
bool do_validate(const std::string& input) const override {
return input.find('@') != std::string::npos
&& input.find('.') != std::string::npos;
}
};
class LengthValidator : public Validator {
std::size_t min_len_;
bool do_validate(const std::string& input) const override {
return input.length() >= min_len_;
}
public:
explicit LengthValidator(std::size_t min) : min_len_{min} {}
};
int main() {
EmailValidator email_v;
LengthValidator len_v{5};
email_v.validate("user@example.com"); // PASS
email_v.validate("invalid"); // FAIL
len_v.validate("hi"); // FAIL
len_v.validate("hello!"); // PASS
return 0;
}RTTI a dynamické_cast
RTTI (Informace o typu běhu) vám umožňuje dotazovat se na skutečný typ polymorfního objektu za běhu. dynamic_cast bezpečně přetypuje základní ukazatel na odvozený ukazatel a v případě selhání přetypování vrátínullptr. Pokud jde o odkazy, neúspěšnýdynamic_castvyvolástd::bad_cast.
typeid(obj) vrací odkazstd::type_info, který lze porovnat a identifikovat typ běhového prostředí.
Používejte RTTI střídmě – časté používánídynamic_castčasto signalizuje problém s návrhem. Upřednostněte virtuální funkce pro typově specifické chování.
Nikdy nevolejte virtuální funkce z konstruktoru nebo destruktoru. Během konstrukce je dynamickým typem objektu aktuálně vytvářená třída, nikoli konečná odvozená třída. Virtuální volání vBase::Base()zavoláBase::method(), nikoliDerived::method()– i když je vytvářený objektDerived. Jedná se o známý zdroj chyb, které může být extrémně obtížné diagnostikovat.
- Virtuální funkce umožňují runtime polymorfismus prostřednictvím vtables a vptrs
- Každá třída s virtuálními funkcemi musí mít destruktor
virtual - Při přepisování vždy používejte
override– zachytí neshody signatur v době kompilace - Čistě virtuální funkce (
= 0) činí třídu abstraktní a nutí odvozené třídy k jejich implementaci - Vzor NVI umožňuje základním třídám vynutit podmínky před/po přidání kolem bodů přizpůsobení
- Vyhněte se volání virtuálních funkcí v konstruktorech/destruktorech – odvozená třída ještě není (nebo již není) plně vytvořena
Kvíz — Otestujte své znalosti
(20 XP)1. Co se stane, když polymorfní základní třída NEMÁ virtuální destruktor?
2. What does the`override`keyword do?
3. Why should you avoid calling virtual functions from a constructor?