250501 TIL - C++에서 깊은 복사와 얕은 복사의 차이, Unreal Engine 5에서의 Cast<T>()의 내부 구현 분석

2025. 5. 1. 21:55·TIL(Today I Learned)

1. C++에서 깊은 복사와 얕은 복사의 차이

  1. 얕은 복사는 말 그대로 값만을 복사하는 방식이고 깊은 복사는 객체가 가리키는 실제 데이터까지 독립된 객체를 생성하는 방식입니다.
  • 얕은 복사는 C++의 기본 복사 생성자, 대입 연산자를 사용하면 일어납니다.
  • 깊은 복사는 사용자 정의 복사 생성자와 대입 연산자를 통해 구현해야 합니다.
  • 얕은 복사의 경우 포인터나 참조 형태의 멤버 변수가 존재하면 메모리 주소만 복사됩니다.
    • 따라서, 원본과 복사본 중 하나가 가리키는 메모리를 변경하면 다른 쪽에도 영향을 주며, 소멸자에서도 두 객체가 동일한 메모리를 해제하려고 할 때 댕글링 포인터와 같은 문제가 발생할 수 있습니다.
  • 깊은 복사의 경우 동적 할당된 자원을 관리하는 객체에는 필수적입니다.
  • 저 같은 경우는 자체 엔진으로 던그리드를 개발하거나 캐슬바니아를 개발할 때, 컴포넌트의 복제가 필요한 경우 깊은 복사를 많이 사용했었습니다.
  • 꼬리질문
  • Rule Of Five에 대해서 아는가?
    • Rule Of Three에 C++11에서 추가된 이동 생성자, 이동 대입 연산자에 대한 것도 추가된 것입니다.
    • Rule Of Three와 동일하게 클래스가 소멸자, 복사 생성자, 복사 대입 연산자, 이동 생성자, 이동 대입 연산자 중 하나라도 정의한다면 나머지도 정의해야 한다는 규칙입니다.
    • 참고 : https://object-world.tistory.com/6
  • 복사 문제와 관련한 해결책이 존재하는가?
    • 멤버 변수로 관리하는 값을 std::shared_ptr로 관리하고 복사 생성자에서 std::make_shared를 사용하여 복제하게 되면 참조 카운팅을 통한 자원 공유로 인해 마지막 참조가 사라질 때만 메모리가 해제되어 더블 프리 문제를 예방할 수 있습니다.
    • std::unique_ptr을 이용하여 복사가 불가능하고 이동만 가능하도록 제어할 수 있으며 복사가 필요하다면 명시적으로 구현하도록 할 수 있습니다.
  • 복사 생성자와 복사 대입 연산자를 제거하는 방법과 그 이유는 무엇인가요?
    • 복사 생성자와 복사 대입 연산자에 = delete 키워드를 사용하여 명시적으로 제거할 수 있습니다.
    • 이유는 다음과 같습니다.
      1. 싱글톤 패턴처럼 객체의 유일성 보장을 위해
      2. 자원을 독점적으로 소유해야 하는 객체인 경우
      3. 복사의 비용이 매우 큰 경우
      4. std::mutex와 같이 복사 시 문제가 발생할 수 있는 객체
        1. 공유 리소스에 대한 독점적인 접근을 보장해야 하는 객체인 std::mutex를 복제하게 된다면 데이터 레이스 현상이 발생할 수 있다.
  • C++에서 복사 생략(Copy Elision)은 무엇인지 설명해라
    • 컴파일러의 최적화 기법으로, 불필요한 객체 복사를 생략하는 것을 의미합니다. 가장 대표적으로는 RVO(Return Value Optimization)와 NRVO(Named Return Value Optimization)입니다.
      • NRVO : 반환 값을 rValue로 호출할 때, 이름을 가진 객체라면 동작하는 최적화 기법으로 디버그 환경에서는 이름을 가진 객체를 리턴하며 이동 생성자를 이용하여 최적화된다. 릴리즈 환경에서는 RVO처럼 동작한다.
      • RVO : 반환 값이 이름을 가지지 않은 형태로 rValue로 호출할 때, 반환 시 대입해야 하는 변수의 메모리 주소에 객체를 생성하는 방식으로 최적화가 동작한다.
  • STL 컨테이너에서 깊은 복사와 얕은 복사는 어떻게 처리되는가?
    • STL 컨테이너는 일반적으로 저장되는 객체를 깊은 복사하여 관리한다.
      • 값 타입 객체는 컨테이너 저장 시 복사 생성자를 통해 깊은 복사된다.
      • 포인터 타입은 포인터 값만 복사되어 얕은 복사가 발생한다.
      • 컨테이너의 복사가 일어나면 저장된 모든 요소가 복사된다.
    • 스마트 포인터를 활용하여 공유 자원을 관리하는 방식도 가능하다.
  • 복사 생성자와 이동 생성자에서noexcept선언은 왜 중요한가?
    • noexcept 선언은 함수가 예외를 던지지 않음을 컴파일러에게 알려주는 역할을 합니다. 특히 이동 생성자와 이동 대입 연산자에 중요합니다.
      1. 컨테이너 최적화: STL 컨테이너는 예외 안전성을 보장하기 위해 noexcept 연산자를 확인합니다. 예를 들어 std::vector의 크기 재할당 시, 요소의 이동 생성자가 noexcept라면 이동 연산을 사용하고, 그렇지 않으면 복사 연산을 사용합니다.
      2. 성능 향상: 예외 처리를 위한 추가 코드가 생성되지 않아 성능이 향상됩니다.
  • 가상 소멸자와 깊은 복사의 관계는 무엇인가?
    • 가상 소멸자와 깊은 복사는 상속 계층에서 중요한 역할을 합니다:
      1. 가상 소멸자의 필요성: 다형성을 갖는 클래스에서는 기본 클래스의 소멸자를 가상으로 선언해야 합니다. 그렇지 않으면 파생 클래스의 객체가 기본 클래스 포인터를 통해 삭제될 때 파생 클래스의 소멸자가 호출되지 않아 메모리 누수가 발생할 수 있습니다.
      2. 깊은 복사와의 관계: 상속 계층에서 깊은 복사를 구현할 때, 기본 클래스와 파생 클래스의 복사 생성자와 대입 연산자가 모두 올바르게 동작해야 합니다.
  • 복사 생성 방지(Copy Prevention)의 다양한 방법은 무엇인가?
    • C++11 이후 방식: = delete 키워드를 사용하여 명시적으로 복사 함수를 삭제합니다.
    • class NoCopy { public: NoCopy() = default; NoCopy(const NoCopy&) = delete; NoCopy& operator=(const NoCopy&) = delete; };
    • C++11 이전 방식: 복사 함수를 private 섹션에 선언하고 구현하지 않습니다.
    • class NoCopyLegacy { private: NoCopyLegacy(const NoCopyLegacy&); NoCopyLegacy& operator=(const NoCopyLegacy&); public: NoCopyLegacy() {} };
    • Boost.Noncopyable 사용: Boost 라이브러리의 noncopyable 클래스를 상속합니다.
    • #include <boost/noncopyable.hpp> class MyClass : private boost::noncopyable { // 복사 생성자와 복사 대입 연산자가 자동으로 삭제됨 };
    • 이동 가능하지만 복사 불가능한 클래스:
    • class MoveOnly { public: MoveOnly() = default; // 복사 불가능 MoveOnly(const MoveOnly&) = delete; MoveOnly& operator=(const MoveOnly&) = delete; // 이동 가능 MoveOnly(MoveOnly&&) noexcept = default; MoveOnly& operator=(MoveOnly&&) noexcept = default; };
  • C++에서 객체의 복사를 방지하는 여러 방법이 있습니다:
  • 복사 생성자에서 발생할 수 있는 자기 할당(Self-Assignment) 문제는 무엇이며 어떻게 방지하나요?
    MyClass obj;
    obj = obj;// 자기 할당 발생
    
    자기 할당이 문제가 되는 이유:
    1. 동적 할당된 메모리를 먼저 해제한 후 복사하면, 자기 자신의 이미 해제된 메모리를 참조하게 됩니다.
    2. 이로 인해 미정의 동작(undefined behavior)이 발생할 수 있습니다.
    방지 방법:
    • 자기 참조 확인: 가장 기본적인 방법으로, 복사 전에 자기 자신인지 확인합니다.
    • cpp MyClass& MyClass::operator=(const MyClass& other) { if (this != &other) {// 자기 할당 검사 delete[] data; size = other.size; data = new int[size]; std::copy(other.data, other.data + size, data); } return *this; }
    • 복사 후 교환(Copy and Swap) 관용구: 예외 안전성도 확보할 수 있는 방법입니다.
    • cpp MyClass& MyClass::operator=(MyClass other) {// 매개변수로 복사본 받음 swap(*this, other);// 복사본과 현재 객체 교환 return *this; // other는 함수 종료 시 소멸되어 이전 자원 정리 } // swap 함수 구현 friend void swap(MyClass& first, MyClass& second) noexcept { using std::swap; swap(first.data, second.data); swap(first.size, second.size); }
    • 스마트 포인터 사용: 자원 관리를 스마트 포인터에 위임하여 자기 할당 문제를 방지합니다.
    • cpp class SafeClass { private: std::unique_ptr<int[]> data; size_t size; public: SafeClass& operator=(const SafeClass& other) { auto temp = std::make_unique<int[]>(other.size); std::copy(other.data.get(), other.data.get() + other.size, temp.get()); data = std::move(temp); size = other.size; return *this; } };
    이러한 방법들을 통해 자기 할당 문제와 예외 안전성 문제를 동시에 해결할 수 있습니다.
  • 자기 할당은 객체가 자기 자신에게 할당되는 상황을 말합니다. 이는 특히 깊은 복사를 구현할 때 문제가 될 수 있습니다.

 

2. Unreal Engine 5에서의 Cast<T>()의 내부 구현 분석

 

Casts.h 파일은 Unreal Engine의 객체 타입 캐스팅 시스템을 정의하는 핵심 헤더 파일이다.

이 파일은 UObject 계층 구조 내에서 객체들을 안전하게 타입 변환하는 다양한 캐스팅 함수와 템플릿을 제공한다.

Cast 함수는 Cast<To>(From* Src)의 형태로 사용하게 됩니다.

내부적으로 구현을 살펴보면 5.5기준으로

  1. From과 To의 타입 크기를 체크하여 불완전한 유형 간에 캐스팅을 시도하는 것인지 검사.
  2. 원본 객체가 nullptr이 아닌지 체크2-2. Editor인 상태면 UE_USE_CAST_FLAGS가 True이고 TCastFlags::Value를 확인하여 해당 클래스의 타입에 대해 CASTCLASS_None 이 아니면 캐스팅 검증을 계속한다.2-2-2. 아니라면 To가 From의 부모 클래스는 아니므로 From이 To의 부모인지 is_base_of_v를 통해 확인한 다음 UClass에서 HasAnyCastFlag를 통해 To의 TCastFlags를 확인하여 어떠한 캐스트 플래그라도 켜져있는지 확인하여 다운캐스팅을 통해 객체를 반환한다.2-3-1. To가 인터페이스이면 원본 객체에서 To의 인터페이스 주소를 가져와 반환2-3-3. IsA로 타입을 확인하여 캐스팅한다.
    1. IsA는 Editor일 때와 아닐 때 다르게 동작한다.
      1. Editor인 경우, UClass 기반으로 SuperClass를 타고 올라가면서 일치하는지 확인한다. 즉, 오래걸린다.
      2. Editor가 아닌 경우, 미리 BaseChain을 만들어 두고, SuperClass가 해당 Chain 안에 있는지 확인한다. Interface를 제외하고는 다중상속을 허용하지 않으므로, 부모 클래스가 해당 자식 클래스의 부모라면 자식 클래스의 Chain 상에 정해진 Depth에 존재한다.
        1. 아래의 두 줄로 부모 클래스가 Chain 내부에 있는지 확인하여 고속으로 캐스팅 확인이 끝난다.
         FORCEINLINE bool IsChildOfUsingStructArray(const FStructBaseChain& Parent) const
        	{
        		int32 NumParentStructBasesInChainMinusOne = Parent.NumStructBasesInChainMinusOne;
        		return NumParentStructBasesInChainMinusOne <= NumStructBasesInChainMinusOne && StructBaseChainArray[NumParentStructBasesInChainMinusOne] == &Parent;
        	}
        
  3. 2-3-2. is_base_of_v를 통해 To가 From의 부모라면 이미 기능을 From이 포함하고 있으므로 그대로 반환
  4. 2-3. Editor인 상태가 아니라면
  5. 2-2-1. std::is_base_of_v라는 C++17이후에 추가된 기능을 사용하여 To가 From의 부모 클래스인지 확인하고 맞다면 업캐스팅을 하여 객체를 반환한다.
  6. 2-1. From이 T인터페이스 인지 확인하고 Interface이면 해당 Interface가 가르키는 Object의 Class를 타고 올라가며 대상 Interface를 구현하고 있는지 확인하고 맞다면 형변환해서 반환한다.

'TIL(Today I Learned)' 카테고리의 다른 글

250424 TIL - 1 - Navigation 동적 변형 설정  (0) 2025.04.24
250421 TIL - Unreal AI 제작 및 제어를 위한 기초  (0) 2025.04.21
250418 TIL - 프로젝트 회고  (0) 2025.04.18
250416 TIL - 컨트롤 릭을 이용한 애니메이션 생성  (0) 2025.04.16
250403 TIL - 캐릭터 분석 결과와 그로 인한 결론  (0) 2025.04.03
'TIL(Today I Learned)' 카테고리의 다른 글
  • 250424 TIL - 1 - Navigation 동적 변형 설정
  • 250421 TIL - Unreal AI 제작 및 제어를 위한 기초
  • 250418 TIL - 프로젝트 회고
  • 250416 TIL - 컨트롤 릭을 이용한 애니메이션 생성
DevJoo1120
DevJoo1120
  • DevJoo1120
    Jin's Programming
    DevJoo1120
  • 전체
    오늘
    어제
    • 분류 전체보기 (142)
      • 포트폴리오 (7)
        • Castlevania: Aria of Sorrow.. (7)
        • [UE5] KILL Everything (0)
      • C++ (0)
      • 라이브러리 (1)
      • 다이렉트X11 (0)
      • Unreal Engine (11)
        • Unreal Document (1)
        • 이것 저것 (8)
        • UI (1)
      • 자료구조 및 알고리즘 (0)
      • 책 정리 (3)
        • 코딩 테스트 합격자 되기 C++ 편 (10)
      • 코딩 테스트 (32)
        • 프로그래머스 (32)
      • 스파르타 코딩 언리얼 1기 (9)
        • 특강 (0)
        • C++와 Unreal Engine으로 3D .. (2)
      • TIL(Today I Learned) (63)
      • 영어 공부 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    C++
    과제
    책 정리
    팀 프로젝트
    정렬
    배열
    코딩테스트
    Til
    Unreal Engine 5
    반복문
    Study
    이중 반복문
    프로그래머스
    코딩 테스트 합격자 되기 c++ 편
    정리
    WINAPI
    코딩 테스트
    스파르타 코딩 클럽
    문자열
    map
  • hELLO· Designed By정상우.v4.10.5
DevJoo1120
250501 TIL - C++에서 깊은 복사와 얕은 복사의 차이, Unreal Engine 5에서의 Cast<T>()의 내부 구현 분석
상단으로

티스토리툴바