UObject의 복사 생성자와 이동 생성자가 삭제되는 위치와, 엔진이 대신 제공하는 함수를 알아봅니다.


돌아가기

프로그래밍 언어

방정현

목차

이동 의미론 : 7. 언리얼 엔진의 구현 - UObject의 복사와 이동

개요

5편에서 언리얼의 MoveTemp·Forward·CopyTemp를, 6편에서 noexcept와 TArray 재할당을 확인했습니다. 이번 글에서는 언리얼 엔진에서 이동 의미론을 쓸 수 없는 UObject를 엔진 소스로 확인해 보겠습니다. 8편에서는 블루프린트 호출을 다룹니다.

검증 엔진은 UE 5.7.0입니다.


1. 삭제된 복사 생성자와 이동 생성자

6편에서 TArray가 원소를 옮길 때 생성자를 호출하지 않고 메모리를 바이트째 복사하는 파괴적 이동을 한다는 것을 확인했습니다. FGCObject는 생성자를 통해 자신의 주소를 GC에 등록하기 때문에 파괴적 이동을 하면 소멸자가 호출되지 않아, GC에는 옮기기 전 주소가 남습니다. 그래서 FGCObject는 TArray에 값으로 담으면 안 되는 타입이었습니다.

UObject도 FGCObject처럼 주소가 GC에 기록되는 타입이어서 TArray에 값으로 담으면 안 됩니다. 그런데 UObject는 제약이 더 강합니다. UObject는 복사 생성자와 이동 생성자가 삭제되어 있어서, UObject를 복사하는 코드도 이동하는 코드도 컴파일되지 않습니다.

UMyObject* A = NewObject<UMyObject>();

UMyObject B = *A;             // 복사 생성자 호출: 컴파일 에러
UMyObject C = MoveTemp(*A);   // 이동 생성자 호출: 컴파일 에러

다만, 삭제하는 방법은 예상과는 살짝 다릅니다. UMyObject의 헤더에는 = delete가 없습니다. 삭제하는 내용은 UHT(Unreal Header Tool)가 .generated.h에 써넣고, GENERATED_BODY() 매크로가 그 헤더를 읽어서 클래스 본문에 적습니다. UHT에서 이 선언을 써넣는 코드는 다음과 같습니다.

//Engine/Source/Programs/Shared/EpicGames.UHT/Exporters/CodeGen/UhtHeaderCodeGeneratorHFile.cs (1479 ~ 1485)

private static StringBuilder AppendCopyConstructorDefinition(StringBuilder builder, UhtClass classObj)
{
	builder.Append("\t/** Deleted move- and copy-constructors, should never be used */ \\\r\n");
	builder.Append('\t').Append(classObj.SourceName).Append('(').Append(classObj.SourceName).Append("&&) = delete; \\\r\n");
	builder.Append('\t').Append(classObj.SourceName).Append("(const ").Append(classObj.SourceName).Append("&) = delete; \\\r\n");
	return builder;
}

classObj.SourceName은 클래스 이름입니다. 클래스가 UMyObject라면 .generated.h에는 UMyObject(UMyObject&&) = delete;와 UMyObject(const UMyObject&) = delete;가 들어갑니다.

new도 마찬가지입니다. UObject의 클래스 본문에는 operator new가 직접 선언되어 있지 않고, UHT가 써넣는 매크로에는 엔진 내부용 operator new만 선언되어 있습니다. 그래서 UObject는 new로 생성할 수 없습니다.


2. Epic이 밝힌 설계

2016년 5월 Epic 직원이 포럼에 남긴 답변은 UObject가 값 타입(value type)으로 만든 클래스가 아니라고 밝힙니다.

Epic 직원의 답변 : UObject didn't generate default move-constructor and move-assignment operator


3. 엔진이 대신 제공하는 함수

복사 생성자와 operator new가 막힌 자리에는 엔진이 대신 쓸 함수를 제공합니다.

  1. UObject를 복사할 수 없습니다. 대신에 엔진은 DuplicateObject를 제공합니다. DuplicateObject는 원본 객체와 새 Outer, 새 이름을 받아서 복사한 객체를 반환합니다.
  2. UObject를 new로 생성할 수 없습니다. 대신에 엔진은 NewObject를 제공합니다. NewObject는 메모리만 할당하지 않습니다. 객체의 Outer와 이름, 플래그를 정하고, 객체의 주소를 엔진에 등록하고, 초기화에 쓸 템플릿을 넘겨서 생성자를 호출합니다. UObject의 생성자는 이 준비가 끝난 상태에서만 동작하므로, NewObject 없이 생성하면 생성자가 "is not being constructed with NewObject"라는 메시지로 Fatal 오류를 냅니다.

4. 포인터가 이동을 대신하는 방식

이동 생성자가 막힌 자리에는 엔진이 대신 제공하는 함수가 없습니다. UObject는 값으로 넘기거나 담을 수 없어서 UMyObject* 같은 포인터로 다루고, 값 타입이 이동으로 하던 일은 포인터가 대신합니다.

값 타입은 큰 내용을 새 변수로 싸게 넘기려고 이동 생성자를 씁니다. 반면, UObject를 넘길 때는 포인터를 복사합니다.

FBigData B = MoveTemp(A);   // 값 타입: A의 내용을 복사하지 않고 B가 넘겨받습니다.
UMyObject* P2 = P1;         // UObject: 주소 값을 복사합니다. 객체는 그 자리에 있습니다.

포인터를 복사하는 것은 주소 값 하나(64비트 기준 8바이트)를 복사하는 것입니다. 그래서 UObject를 넘기는 코드에서는 MoveTemp를 쓸 일이 없습니다. 포인터를 복사해도 객체는 하나이고, 두 번째 객체가 필요할 때만 앞에서 본 DuplicateObject를 호출합니다.

컨테이너에 담을 때도 포인터를 씁니다. 글 첫머리에서 UObject는 TArray에 값으로 담으면 안 된다고 했습니다. TArray에는 UObject 대신 포인터를 담습니다.

UPROPERTY()
TArray<UMyObject*> Items;

Items가 재할당될 때 옮겨지는 것은 포인터 값이고, UMyObject 객체는 옮겨지지 않습니다. 객체가 옮겨지지 않으므로 NewObject가 엔진에 등록한 주소도 그대로 유지됩니다. 멤버 변수로 선언할 때는 UPROPERTY()를 붙입니다. UPROPERTY()가 없으면 GC가 이 배열을 알지 못해서, 배열이 가리키는 객체를 의도치 않게 해제할 수 있습니다.


정리

이번 글에서 확인한 내용은 다음과 같습니다.

다음 글에서는 블루프린트에 노출하는 함수에서 rvalue 참조와 스마트 포인터가 거절되는 이유와, 블루프린트가 넘긴 구조체가 C++ 함수에 전달되는 방식을 확인합니다.

다음 글로 바로가기 : 이동 의미론 : 8. 언리얼 엔진의 구현 - 블루프린트 호출

참고


E-mail : [email protected] Phone number : (+82) 010 3902 8624