Как объявить интерфейс в C++? Чисто виртуальные функции и абстрактные классы
Интерфейсы в C++ через абстрактные классы
В C++ нет отдельного ключевого слова interface, как в Java или C#. Вместо этого интерфейсы реализуются с помощью абстрактных классов, содержащих только чисто виртуальные функции (pure virtual functions). Ваш код с = 0 — это правильный путь, но есть важные нюансы.
Базовый синтаксис интерфейса
class IShape {
public:
virtual ~IShape() = default; // Виртуальный деструктор!
virtual double area() const = 0;
virtual void draw() const = 0;
};
Ключевые моменты:
- Чисто виртуальные функции — объявляются с
= 0в конце. Это делает класс абстрактным: нельзя создать объект такого класса напрямую. - Виртуальный деструктор — обязателен для интерфейсов. Если вы удаляете объект производного класса через указатель на интерфейс, без виртуального деструктора поведение не определено.
- Константные методы — хороший тон для методов, которые не изменяют объект (как
area()иdraw()).
Реализация интерфейса
class Circle : public IShape {
private:
double radius;
public:
explicit Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
void draw() const override {
std::cout << "Рисуем круг с радиусом " << radius << std::endl;
}
};
class Rectangle : public IShape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override {
return width * height;
}
void draw() const override {
std::cout << "Рисуем прямоугольник " << width << "x" << height << std::endl;
}
};
Использование через указатели на интерфейс
void processShape(const IShape& shape) {
shape.draw();
std::cout << "Площадь: " << shape.area() << std::endl;
}
int main() {
Circle circle(5.0);
Rectangle rect(3.0, 4.0);
processShape(circle); // Полиморфизм через ссылку
processShape(rect);
// Динамический полиморфизм через указатели
std::vector<std::unique_ptr<IShape>> shapes;
shapes.push_back(std::make_unique<Circle>(2.0));
shapes.push_back(std::make_unique<Rectangle>(4.0, 5.0));
for (const auto& shape : shapes) {
shape->draw();
}
return 0;
}
Множественное наследование интерфейсов
C++ поддерживает множественное наследование, что позволяет классу реализовывать несколько интерфейсов:
class IPrintable {
public:
virtual ~IPrintable() = default;
virtual void print() const = 0;
};
class ISerializable {
public:
virtual ~ISerializable() = default;
virtual std::string serialize() const = 0;
};
class Document : public IShape, public IPrintable, public ISerializable {
// Реализация всех чисто виртуальных методов
};
Интерфейс с общей реализацией (default methods)
C++11 и новее позволяют задавать реализацию по умолчанию для чисто виртуальных функций:
class ILogger {
public:
virtual ~ILogger() = default;
virtual void log(const std::string& msg) = 0;
virtual void logError(const std::string& msg) {
log("[ERROR] " + msg); // Общая реализация
}
};
Однако класс всё равно остаётся абстрактным, пока log() не будет реализован.
Правила хорошего тона
- Используйте
overrideдля методов-реализаций — это защитит от опечаток. - Делайте методы интерфейса
const, если они не меняют состояние. - Для владения полиморфными объектами используйте
std::unique_ptrилиstd::shared_ptr. - Избегайте сырых указателей на интерфейсы в современном C++.
Альтернативный подход: концепции (C++20) и статический полиморфизм
Если вам не нужен динамический полиморфизм (виртуальные функции и таблицы VMT), можно использовать концепции из C++20 для проверки интерфейсов на этапе компиляции. Это даёт лучшую производительность и безопасность типов.
template <typename T>
concept Shape = requires(const T& shape) {
{ shape.area() } -> std::convertible_to<double>;
{ shape.draw() } -> std::same_as<void>;
};
class Circle {
double radius;
public:
Circle(double r) : radius(r) {}
double area() const { return 3.14 * radius * radius; }
void draw() const { std::cout << "Круг\n"; }
};
template <Shape S>
void processShape(const S& shape) {
shape.draw();
std::cout << shape.area() << '\n';
}
Этот подход называют "duck typing" через шаблоны. Он не требует наследования, виртуальных функций или общего базового класса. Если тип соответствует концепции, он "является" формой.
Когда использовать:
- Когда производительность критична (нет накладных расходов на VMT).
- Когда вы работаете с типами, которые не можете наследовать (например, из сторонних библиотек).
- В шаблонном метапрограммировании.
Недостатки:
- Все коллекции должны быть шаблонными (нет
std::vector<IShape*>). - Ошибки компиляции могут быть сложнее для понимания.
- Требует C++20.
Ответов пока нет.
Войдите чтобы оставить ответ