← Назад к вопросам

Что такое статический полиморфизм?

2.0 Middle🔥 171 комментариев
#ООП и проектирование#Язык C++

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Статический полиморфизм в C++

Определение

Статический полиморфизм — это способность писать код, который работает с разными типами на этапе компиляции. Тип определяется компилятором, а не во время выполнения программы.

В отличие от динамического полиморфизма (virtual функции), где тип проверяется в runtime, здесь всё решается на compile-time.

Два основных механизма

1. Шаблоны (Templates) — Template-based polymorphism 2. Перегрузка функций (Overloading) — Ad-hoc polymorphism

1. Шаблоны — самый мощный инструмент

Шаблоны позволяют писать обобщённый код, который компилятор специализирует для каждого типа:

// Обобщённая функция
template <typename T>
void print(T value) {
    std::cout << value << std::endl;
}

int main() {
    print(42);           // Компилятор генерирует print<int>
    print(3.14);         // Компилятор генерирует print<double>
    print("hello");      // Компилятор генерирует print<const char*>
    return 0;
}

Компилятор создаёт отдельный код для каждого типа. Это называется instantiation.

Практический пример: обобщённый контейнер

// Template класс
template <typename T>
class Stack {
private:
    std::vector<T> data;
    
public:
    void push(const T& value) {
        data.push_back(value);
    }
    
    T pop() {
        T value = data.back();
        data.pop_back();
        return value;
    }
    
    bool empty() const {
        return data.empty();
    }
};

int main() {
    Stack<int> intStack;
    intStack.push(1);
    intStack.push(2);
    std::cout << intStack.pop() << std::endl;  // 2
    
    Stack<std::string> strStack;
    strStack.push("hello");
    strStack.push("world");
    std::cout << strStack.pop() << std::endl;  // world
    
    // Компилятор создал ДВА разных класса
    // Stack<int> и Stack<std::string>
}

2. Перегрузка функций — Ad-hoc полиморфизм

// Одна функция, разные реализации для разных типов
void process(int x) {
    std::cout << "Обработка int: " << x << std::endl;
}

void process(double x) {
    std::cout << "Обработка double: " << x << std::endl;
}

void process(const std::string& x) {
    std::cout << "Обработка string: " << x << std::endl;
}

int main() {
    process(42);        // Вызывает process(int)
    process(3.14);      // Вызывает process(double)
    process("hello");   // Вызывает process(const std::string&)
}

// Компилятор на этапе компиляции выбирает нужную функцию

Статический vs Динамический полиморфизм

Статический (compile-time):

template <typename T>
void process(T value) {
    std::cout << sizeof(T) << std::endl;
}

int main() {
    process(42);        // sizeof(int) — известно в compile-time
    process(3.14);      // sizeof(double) — известно в compile-time
    
    // Каждый вызов — разный код в бинаре
}

Динамический (runtime):

class Shape {
public:
    virtual void draw() = 0;  // virtual — динамическое связывание
};

class Circle : public Shape {
public:
    void draw() override { std::cout << "Circle\n"; }
};

class Square : public Shape {
public:
    void draw() override { std::cout << "Square\n"; }
};

void render(Shape* shape) {
    shape->draw();  // Какой класс? Узнаем только в runtime
}

int main() {
    Circle c;
    Square s;
    
    render(&c);  // Один и тот же код, разные результаты
    render(&s);
}

CRTP — Curiously Recurring Template Pattern

Это продвинутая техника статического полиморфизма без virtual функций:

// Базовый класс template
template <typename Derived>
class Base {
public:
    void operation() {
        static_cast<Derived*>(this)->operationImpl();
    }
};

// Производные классы
class DerivedA : public Base<DerivedA> {
public:
    void operationImpl() {
        std::cout << "DerivedA operation\n";
    }
};

class DerivedB : public Base<DerivedB> {
public:
    void operationImpl() {
        std::cout << "DerivedB operation\n";
    }
};

int main() {
    DerivedA a;
    DerivedB b;
    
    a.operation();  // Вызывает DerivedA::operationImpl
    b.operation();  // Вызывает DerivedB::operationImpl
    
    // Полиморфизм БЕЗ virtual!
    // Нет вызовов через vtable, всё инлайнится
}

Шаблонная специализация

Различная реализация для разных типов:

// Обобщённая версия
template <typename T>
struct Traits {
    static void describe() {
        std::cout << "Обобщённый тип\n";
    }
};

// Специализация для int
template <>
struct Traits<int> {
    static void describe() {
        std::cout << "Целое число (int)\n";
    }
};

// Специализация для double
template <>
struct Traits<double> {
    static void describe() {
        std::cout << "Число с плавающей точкой (double)\n";
    }
};

int main() {
    Traits<int>::describe();      // Целое число (int)
    Traits<double>::describe();   // Число с плавающей точкой
    Traits<char>::describe();     // Обобщённый тип
}

Преимущества статического полиморфизма

1. Производительность

Код инлайнится и оптимизируется компилятором:

// Статический — компилятор может полностью инлайнить
template <typename T>
T add(T a, T b) { return a + b; }

auto result = add(1, 2);  // Может быть заменено просто на 3

// Динамический — никогда не знаем, какой вызов
Shape* s = ...;
s->draw();  // Всегда vtable lookup

2. Отсутствие runtime overhead

Нет виртуальных таблиц, нет вызовов через указатели.

3. Размер типа известен в compile-time

template <typename T>
void printSize() {
    std::cout << sizeof(T) << std::endl;  // Можно использовать
}

Недостатки

1. Размер бинарного файла (code bloat)

Для каждого типа генерируется отдельный код:

void process(int x) { /* код */ }
void process(double x) { /* тот же код, но для double */ }
void process(float x) { /* тот же код, но для float */ }
// Трёхкратное дублирование кода

2. Сложность компиляции

Шаблоны замедляют компиляцию.

3. Ошибки компиляции сложнее

template <typename T>
void foo(T t) {
    t.someMethod();  // Если T не имеет someMethod — ошибка компиляции
}

foo(42);  // Ошибка: int не имеет someMethod
// Ошибка будет в определении foo, а не в точке вызова

Когда использовать?

Статический полиморфизм:

  • Контейнеры (std::vector<T>, std::queue<T>)
  • Алгоритмы, чувствительные к производительности
  • Математические библиотеки (Eigen, Armadillo)

Динамический полиморфизм:

  • Иерархии классов (фигуры, животные)
  • Плагины и расширяемость
  • Когда тип известен только в runtime

Заключение

  • Статический полиморфизм — выбор типа в compile-time
  • Основной механизм: templates и function overloading
  • Преимущества: производительность, отсутствие overhead
  • Недостатки: код bloat, сложность ошибок, время компиляции
  • CRTP — продвинутая техника для virtual-подобного поведения
  • Используй статический для максимальной производительности
  • Используй динамический для гибкости архитектуры