[2026] C++ 슬라이싱 문제 | 객체가 잘렸어요 상속 복사 에러 해결
이 글의 핵심
C++ 슬라이싱 문제의 C++, 슬라이싱, 객체가, 들어가며: 파생 클래스를 복사했더니 데이터가 사라졌어요를 실전 예제와 함께 상세히 설명합니다.
들어가며: “파생 클래스를 복사했더니 데이터가 사라졌어요"
"다형성이 작동하지 않아요”
C++에서 파생 클래스 객체를 베이스 클래스 타입으로 복사하면, 파생 클래스의 멤버가 잘려나가는 슬라이싱(Slicing) 문제가 발생합니다.
// ❌ 슬라이싱 문제
class Animal {
public:
virtual void speak() {
std::cout << "Animal sound\n";
}
};
class Dog : public Animal {
std::string name_;
public:
Dog(const std::string& name) : name_(name) {}
void speak() override {
std::cout << name_ << " barks\n";
}
};
void makeSound(Animal animal) { // ❌ 값 전달
animal.speak();
}
int main() {
Dog dog("Buddy");
makeSound(dog); // ❌ 슬라이싱 발생
// 출력: Animal sound (Dog가 잘림!)
}
이 글에서 다루는 것:
- 슬라이싱 문제란?
- 다형성 손실
- 참조·포인터로 해결
- 복사 방지 패턴
실무에서 마주한 현실
개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.
목차
1. 슬라이싱 문제란?
슬라이싱 발생
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Base {
public:
int x = 1;
virtual void foo() {
std::cout << "Base::foo\n";
}
};
class Derived : public Base {
public:
int y = 2; // 파생 클래스 멤버
void foo() override {
std::cout << "Derived::foo\n";
}
};
int main() {
Derived d;
Base b = d; // ❌ 슬라이싱 발생
std::cout << b.x << '\n'; // 1
// std::cout << b.y << '\n'; // 컴파일 에러: y가 없음
b.foo(); // Base::foo (다형성 손실)
}
문제:
y멤버 손실- 다형성 손실 (virtual 무시)
- vtable 포인터 복사 안 됨
2. 다형성 손실
예시 1: 함수 인자
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 값 전달
void process(Animal animal) { // 슬라이싱
animal.speak(); // Animal::speak 호출
}
Dog dog("Buddy");
process(dog); // Animal sound (다형성 손실)
// ✅ 참조 전달
void process(Animal& animal) { // 슬라이싱 없음
animal.speak(); // Dog::speak 호출
}
process(dog); // Buddy barks (다형성 유지)
예시 2: 컨테이너
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ vector<Base>
std::vector<Animal> animals;
animals.push_back(Dog("Buddy")); // 슬라이싱
animals[0].speak(); // Animal sound
// ✅ vector<unique_ptr<Base>>
std::vector<std::unique_ptr<Animal>> animals;
animals.push_back(std::make_unique<Dog>("Buddy"));
animals[0]->speak(); // Buddy barks
3. 해결책: 참조·포인터
해결책 1: 참조 사용
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 참조
void process(const Animal& animal) {
animal.speak();
}
Dog dog("Buddy");
process(dog); // Buddy barks
해결책 2: 포인터 사용
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 포인터
void process(Animal* animal) {
if (animal) {
animal->speak();
}
}
Dog dog("Buddy");
process(&dog); // Buddy barks
해결책 3: 스마트 포인터
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ✅ unique_ptr
void process(std::unique_ptr<Animal> animal) {
animal->speak();
}
process(std::make_unique<Dog>("Buddy")); // Buddy barks
// ✅ shared_ptr
void process(std::shared_ptr<Animal> animal) {
animal->speak();
}
process(std::make_shared<Dog>("Buddy")); // Buddy barks
4. 복사 방지 패턴
패턴 1: 복사 생성자 delete
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Animal {
public:
Animal() = default;
Animal(const Animal&) = delete; // 복사 금지
Animal& operator=(const Animal&) = delete;
virtual ~Animal() = default;
virtual void speak() = 0;
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Bark\n";
}
};
int main() {
Dog dog;
// Animal animal = dog; // 컴파일 에러
Animal& ref = dog; // OK
}
패턴 2: protected 복사 생성자
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Animal {
protected:
Animal(const Animal&) = default; // 파생 클래스만 복사 가능
public:
Animal() = default;
virtual ~Animal() = default;
virtual void speak() = 0;
};
int main() {
Dog dog1;
// Animal animal = dog1; // 컴파일 에러
Dog dog2 = dog1; // OK (파생 클래스 복사)
}
패턴 3: clone 메서드
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Animal {
public:
virtual ~Animal() = default;
virtual std::unique_ptr<Animal> clone() const = 0;
virtual void speak() = 0;
};
class Dog : public Animal {
std::string name_;
public:
Dog(const std::string& name) : name_(name) {}
std::unique_ptr<Animal> clone() const override {
return std::make_unique<Dog>(*this);
}
void speak() override {
std::cout << name_ << " barks\n";
}
};
int main() {
Dog dog("Buddy");
std::unique_ptr<Animal> cloned = dog.clone();
cloned->speak(); // Buddy barks
}
정리
슬라이싱 방지 규칙
| 방법 | 장점 | 단점 |
|---|---|---|
| 참조 | 간단, 다형성 유지 | null 불가 |
| 포인터 | null 가능, 다형성 유지 | 수동 관리 |
| 스마트 포인터 | 자동 관리, 다형성 유지 | 오버헤드 |
| 복사 금지 | 슬라이싱 방지 | 복사 불가 |
핵심 규칙
- 값 전달 금지 (참조·포인터 사용)
- vector
금지 (vector<unique_ptr > 사용) - 복사 생성자 delete (슬라이싱 방지)
- clone 메서드 (다형성 복사)
체크리스트
- 함수 인자가 참조나 포인터인가?
- 컨테이너가 포인터를 저장하는가?
- 복사 생성자가 delete되어 있는가?
- clone 메서드가 구현되어 있는가?
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 상속 | Inheritance 완벽 가이드
- C++ 가상 함수 | virtual function 가이드
- C++ 다형성 | Polymorphism 가이드
- C++ 스마트 포인터 | unique_ptr·shared_ptr
마치며
슬라이싱 문제는 다형성을 손실시키는 위험한 버그입니다. 핵심 원칙:
- 값 전달 금지
- 참조·포인터 사용
- 복사 생성자 delete 다형성 객체는 절대 값으로 전달하지 마세요. 참조나 포인터를 사용하세요. 다음 단계: 슬라이싱을 이해했다면, C++ 다형성 가이드에서 더 깊이 배워보세요.