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. 슬라이싱 문제란?
  2. 다형성 손실
  3. 해결책: 참조·포인터
  4. 복사 방지 패턴
  5. 정리

1. 슬라이싱 문제란?

슬라이싱 발생

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: 함수 인자

// ❌ 값 전달
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: 컨테이너

// ❌ 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: 참조 사용

// ✅ 참조
void process(const Animal& animal) {
    animal.speak();
}

Dog dog("Buddy");
process(dog);  // Buddy barks

해결책 2: 포인터 사용

// ✅ 포인터
void process(Animal* animal) {
    if (animal) {
        animal->speak();
    }
}

Dog dog("Buddy");
process(&dog);  // Buddy barks

해결책 3: 스마트 포인터

// ✅ 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

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 복사 생성자

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 메서드

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 가능, 다형성 유지수동 관리
스마트 포인터자동 관리, 다형성 유지오버헤드
복사 금지슬라이싱 방지복사 불가

핵심 규칙

  1. 값 전달 금지 (참조·포인터 사용)
  2. vector 금지 (vector<unique_ptr> 사용)
  3. 복사 생성자 delete (슬라이싱 방지)
  4. clone 메서드 (다형성 복사)

체크리스트

  • 함수 인자가 참조나 포인터인가?
  • 컨테이너가 포인터를 저장하는가?
  • 복사 생성자가 delete되어 있는가?
  • clone 메서드가 구현되어 있는가?

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ 상속 | Inheritance 완벽 가이드
  • C++ 가상 함수 | virtual function 가이드
  • C++ 다형성 | Polymorphism 가이드
  • C++ 스마트 포인터 | unique_ptr·shared_ptr

마치며

슬라이싱 문제다형성을 손실시키는 위험한 버그입니다.

핵심 원칙:

  1. 값 전달 금지
  2. 참조·포인터 사용
  3. 복사 생성자 delete

다형성 객체절대 값으로 전달하지 마세요. 참조나 포인터를 사용하세요.

다음 단계: 슬라이싱을 이해했다면, C++ 다형성 가이드에서 더 깊이 배워보세요.


관련 글

  • C++ 시리즈 전체 보기
  • C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
  • C++ ADL |
  • C++ Aggregate Initialization |