Avatar ·

Что такое срезка объектов (object slicing) в C++ и как её избежать?

Привет! Я изучаю C++ и столкнулся с проблемой, которую не могу понять. У меня есть базовый класс `Shape` и производный класс `Circle`. Когда я передаю объект `Circle` в функцию, которая принимает `Shape` по значению, я теряю данные и поведение, специфичные для `Circle`. Например: ```cpp #include #include class Shape { public: virtual void draw() const { std::cout << "Drawing Shape\n"; } }; class Circle : public Shape { public: void draw() const override { std::cout << "Drawing Circle\n"; } double radius = 5.0; }; void printShape(Shape s) { // Принимаем по значению s.draw(); // std::cout << s.radius; // Ошибка! radius нет в Shape } int main() { Circle c; printShape(c); // Выводит "Drawing Shape", а не "Drawing Circle"! return 0; } ``` Я ожидал, что `draw()` вызовется из `Circle`, но вызывается версия из `Shape`. Кроме того, я не могу получить доступ к `radius`. В интернете это называют "object slicing". Можете объяснить, что происходит "под капотом" и как правильно передавать объекты производных классов? Также хотелось бы узнать, как избежать этой проблемы при работе с контейнерами, например, `std::vector`. <<>>

Что такое срезка объектов (object slicing)?

Срезка объектов — это потеря данных и полиморфного поведения при копировании объекта производного класса в объект базового класса по значению. В C++ объекты хранятся фиксированного размера. Когда вы присваиваете Circle переменной типа Shape, компилятор создаёт новый объект Shape, копируя только ту часть Circle, которая относится к Shape. Всё, что специфично для Circle (например, radius и переопределённый draw()), отбрасывается — "срезается".

Почему это происходит?

В вашем примере void printShape(Shape s) принимает аргумент по значению. При вызове printShape(c) компилятор создаёт копию типа Shape из Circle. Размер Shape меньше, чем Circle, поэтому лишняя часть просто не копируется. Виртуальная функция draw() вызывается через таблицу виртуальных функций (vtable), но для нового объекта Shape vtable указывает на метод Shape::draw(), а не Circle::draw().

Как исправить?

Используйте указатели или ссылки, чтобы сохранить полиморфизм:

void printShape(const Shape& s) { // Ссылка
    s.draw(); // Вызовется Circle::draw()
}

void printShapePtr(const Shape* s) { // Указатель
    if (s) s->draw();
}

int main() {
    Circle c;
    printShape(c);       // Работает через ссылку
    printShapePtr(&c);   // Работает через указатель
    return 0;
}

Проблема с контейнерами

std::vector<Shape> хранит объекты по значению, поэтому срезка происходит при вставке:

std::vector<Shape> shapes;
shapes.push_back(Circle()); // Срезка! Хранится только Shape

Решение — хранить указатели (лучше умные):

#include <memory>
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>());
shapes[0]->draw(); // Drawing Circle

Ключевые правила

  • Никогда не передавайте полиморфные объекты по значению — используйте ссылки (&) или указатели (*).
  • Для контейнеров используйте std::vector<std::unique_ptr<Base>> или std::vector<Base*> (если владение не ваше).
  • Виртуальные функции работают только через ссылки/указатели — по значению вызывается метод базового класса.
  • Срезка также происходит при передаче в конструктор копирования базового класса: Shape s = circle;.

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

<<>>

Альтернативный подход: шаблоны и CRTP

Если вам не нужен полиморфизм времени выполнения, можно использовать статический полиморфизм через шаблоны. Это полностью исключает срезку, так как тип определяется на этапе компиляции:

template <typename T>
void printShape(const T& s) {
    s.draw();
    std::cout << s.radius; // Работает, если T — Circle
}

struct Circle {
    void draw() const { std::cout << "Drawing Circle\n"; }
    double radius = 5.0;
};

struct Square {
    void draw() const { std::cout << "Drawing Square\n"; }
    double side = 3.0;
};

int main() {
    Circle c;
    Square sq;
    printShape(c);  // Drawing Circle, radius = 5
    printShape(sq); // Drawing Square, side = 3
    return 0;
}

Этот подход не требует виртуальных функций и наследования. Он быстрее (раннее связывание) и безопаснее с точки зрения срезки. Однако вы не сможете хранить разные типы в одном контейнере без дополнительных ухищрений (например, std::variant или std::any).

Для случаев, где нужен именно полиморфизм времени выполнения (например, гетерогенные коллекции), используйте указатели/ссылки, как описано в первом ответе. Выбор зависит от задачи: статический полиморфизм — для производительности, динамический — для гибкости.