[C++] static_cast 를 통한 객체 캐스팅

Will-Big·2025년 3월 7일
0

Cpp

목록 보기
6/7

최근 주변에서 static_cast 를 포인터나 참조, 혹은 r-value 가 아닌 실제 객체에 사용했을 때 어떻게 동작하는지에 대한 질문이 들어왔습니다. 제게 “static_cast 가 실행될 때 임시 객체가 생성된다”라는 설명을 들었는데, 이에 대해 자세히 정리해보고자 합니다.

1. 실제 객체에 static_cast 를 사용하면 무슨 일이 일어날까?

C++에서 static_cast 를 실제 객체에 적용하면, 대상 타입의 임시 객체(temporary object)가 새로 생성됩니다.
즉,

  • 복사 생성자 또는 변환 연산자를 통해 원본 객체의 해당 부분(예를 들어, 파생 클래스의 Base 부분)만 복사합니다.
  • 이때, 파생 클래스에서 추가된 멤버들은 복사되지 않으며, 그 결과 객체 슬라이싱(object slicing)이 발생하게 됩니다.

아래 예시 코드를 통해 이를 살펴볼 수 있습니다:

#include <iostream>
using namespace std;

class Base {
public:
    int base_val;
    Base(int val = 0) : base_val(val) {}

    virtual void show() const {
        cout << "Base: " << base_val << "\n";
    }
};

class Derived : public Base {
public:
    int derived_val;
    Derived(int base, int derived) : Base(base), derived_val(derived) {}

    // Derived 클래스의 show()는 파생 클래스의 추가 정보를 출력합니다.
    void show() const override {
        cout << "Derived: " << base_val << ", " << derived_val << "\n";
    }
};

int main() {
    Derived d(1, 2);

    // 객체를 값으로 static_cast 하면, 새로운 Base 객체(임시 객체)가 생성되고,
    // Derived의 추가 멤버(derived_val)는 잘려나갑니다.
    Base b = static_cast<Base>(d);
    cout << "After object static_cast (객체 슬라이싱 발생):\n";
    b.show();  // Base::show() 호출, 결과: "Base: 1"

    // 포인터나 참조를 static_cast 하면, 원본 객체를 가리키므로 슬라이싱이 발생하지 않습니다.
    Base& b_ref = static_cast<Base&>(d);
    cout << "After reference static_cast (슬라이싱 없음):\n";
    b_ref.show();  // Derived::show() 호출, 결과: "Derived: 1, 2"

    return 0;
}

위 코드에서 Base b = static_cast<Base>(d); 는 Derived 객체 d 의 Base 부분만을 복사하여 임시 객체를 만들고, 그 결과로 객체 슬라이싱이 발생합니다. 반면, Base& b_ref = static_cast<Base&>(d); 는 단순히 원본 객체를 참조하기 때문에, 가상 함수 호출 시 실제 객체인 Derived 의 함수가 호출됩니다.

2. 복사 생성자를 삭제하면 어떻게 될까?

만약 객체의 복사 생성자가 명시적으로 삭제되었다면, static_cast 를 통해 값 변환(즉, 임시 객체 생성을 요구하는 변환)을 수행할 때 컴파일 오류가 발생합니다.
즉, static_cast 로 객체를 값으로 변환하려고 하면 내부적으로 복사 생성자(또는 이동 생성자)가 호출되는데, 복사 생성자가 삭제된 경우 그 호출이 불가능하여 컴파일러가 에러를 내게 됩니다.

예를 들어보면:

#include <iostream>
using namespace std;

class Base {
public:
    int base_val;
    Base(int val = 0) : base_val(val) {}

    // 복사 생성자를 명시적으로 삭제
    Base(const Base&) = delete;

    virtual void show() const {
        cout << "Base: " << base_val << "\n";
    }
};

class Derived : public Base {
public:
    int derived_val;
    Derived(int base, int derived) : Base(base), derived_val(derived) {}

    void show() const override {
        cout << "Derived: " << base_val << ", " << derived_val << "\n";
    }
};

int main() {
    Derived d(1, 2);

    // 아래 코드는 컴파일 에러를 발생시킵니다.
    // static_cast 를 사용하면 임시 객체 생성을 위해 복사 생성자가 호출되는데, 
    // 복사 생성자가 삭제되어 있으므로 컴파일 되지 않습니다.
    // Base b = static_cast<Base>(d);

    // 반면, 참조를 이용한 캐스팅은 임시 객체 생성을 요구하지 않으므로 정상적으로 작동합니다.
    Base& b_ref = static_cast<Base&>(d);
    b_ref.show();  // 올바르게 Derived::show() 호출, 결과: "Derived: 1, 2"

    return 0;
}

이처럼 복사 생성자가 삭제된 클래스에 대해 객체를 값으로 static_cast 하려고 하면 복사 생성자가 필요하기 때문에 컴파일 오류가 발생합니다.
반면, 포인터나 참조를 사용한 캐스팅은 객체 복사가 일어나지 않으므로 이러한 문제가 발생하지 않습니다.

3. 객체 슬라이싱과 가상 함수 포인터(virtual function pointer)의 메모리 레이아웃

객체 슬라이싱이 발생하면, 파생 클래스의 추가 데이터는 새로 생성된 임시 객체에 복사되지 않습니다. 이와 함께 가상 함수 포인터(vptr) 의 관리에도 중요한 변화가 있습니다.

  • 메모리 레이아웃 측면:
    일반적으로 C++ 객체의 메모리 레이아웃은 vptr(만약 가상 함수가 있다면)와 데이터 멤버들로 구성됩니다.

    • Derived 객체는 자신만의 vptr(보통 Derived 클래스의 vtable을 가리킴)과 추가 멤버들을 포함합니다.
    • 하지만, 객체 슬라이싱을 통해 Base 임시 객체가 생성되면, 이 객체는 Base 클래스의 복사 생성자를 통해 만들어집니다.
  • 가상 함수 호출과 vptr의 역할:
    임시 Base 객체는 Base 타입의 객체로서 생성되기 때문에,

    • 생성 과정에서 Base 클래스의 생성자(또는 복사 생성자)가 실행되고,
    • 이에 따라 vptr은 Base 클래스의 vtable을 가리키도록 초기화됩니다.

    결과적으로, 임시 객체에서는 가상 함수 호출 시 Base 클래스의 구현이 사용됩니다.
    즉, Derived 객체였던 원본과는 달리, 임시 객체는 오직 Base 클래스의 특성만을 반영하게 됩니다.

이러한 메모리 레이아웃 관리 방식은 C++ 컴파일러가 객체의 타입 안전성을 보장하기 위해 내부적으로 처리하는 부분이며, 개발자가 직접 개입할 수 없는 구현 세부사항입니다.

4. C++ 이와 같이 설계된 이유

C++는 값(value) 세만틱스를 기본 원칙으로 채택하고 있습니다. 이 설계 철학에는 몇 가지 중요한 이유가 있습니다.

  • 명시적 복사와 불변성:
    객체를 값으로 전달하거나 복사할 때, 원본 객체와는 독립적인 새로운 객체가 생성되는 것이 일반적입니다.
    이는 프로그램의 다른 부분에서 발생할 수 있는 부작용(side effect)을 줄이고, 객체의 불변성을 유지하는 데 기여합니다.

  • 객체 슬라이싱의 명시성:
    파생 클래스의 추가 멤버나 특성을 의도적으로 사용하지 않고, 기본 클래스의 부분만 필요할 때 객체 슬라이싱이 발생하도록 하여,
    개발자가 슬라이싱의 결과를 명확하게 인지하고 처리할 수 있도록 합니다.

  • 안전한 다형성 구현:
    만약 원본 객체의 다형적 특성을 그대로 유지하고 싶다면, 포인터나 참조를 사용하도록 유도합니다.
    이렇게 함으로써, 개발자가 의도한 경우에만 다형성을 활용할 수 있게 하여, 예기치 않은 동작을 방지합니다.

즉, static_cast 가 임시 객체를 생성하여 슬라이싱을 발생시키는 설계는,

  • 값 복사 시 명확한 독립 객체 생성
  • 개발자에게 명시적 의도 전달
  • 다형적 동작의 선택적 활용
    등의 C++ 설계 철학과 부합합니다.

결론

실제 객체에 static_cast 를 사용하면,

  • 대상 타입에 맞는 임시 객체가 생성되고,
  • 이 과정에서 객체 슬라이싱이 발생하여 파생 클래스의 추가 멤버와 가상 함수 정보가 손실됩니다.

이때, 새로 생성된 임시 객체의 가상 함수 포인터(vptr) 는 Base 클래스의 vtable을 가리키도록 설정되어, 가상 함수 호출 시 Base 클래스의 구현이 실행됩니다.
이러한 설계는 C++의 값 세만틱스명시적 복사 원칙에 기반한 것으로, 개발자에게 보다 명확한 의도 표현과 안전한 다형성 사용을 유도합니다.

profile
개발자가 되고싶어요

0개의 댓글