Что такое срезка объектов (object slicing) в C++ и как её избежать?
Что такое срезка объектов (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).
Для случаев, где нужен именно полиморфизм времени выполнения (например, гетерогенные коллекции), используйте указатели/ссылки, как описано в первом ответе. Выбор зависит от задачи: статический полиморфизм — для производительности, динамический — для гибкости.
Ответов пока нет.
Войдите чтобы оставить ответ