UObject의 복사 생성자와 이동 생성자가 삭제되는 위치와, 엔진이 대신 제공하는 함수를 알아봅니다.
돌아가기
목차
5편에서 언리얼의 MoveTemp·Forward·CopyTemp를, 6편에서 noexcept와 TArray 재할당을 확인했습니다. 이번 글에서는 언리얼 엔진에서 이동 의미론을 쓸 수 없는 UObject를 엔진 소스로 확인해 보겠습니다. 8편에서는 블루프린트 호출을 다룹니다.
UObject의 복사 생성자와 이동 생성자가 삭제되는 위치와 Epic이 밝힌 설계UObject로 할 수 없는 일과 엔진이 제공하는 것, 그리고 포인터가 이동을 대신하는 방식검증 엔진은 UE 5.7.0입니다.
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로 생성할 수 없습니다.
2016년 5월 Epic 직원이 포럼에 남긴 답변은 UObject가 값 타입(value type)으로 만든 클래스가 아니라고 밝힙니다.
Epic 직원의 답변 : UObject didn't generate default move-constructor and move-assignment operator
복사 생성자와 operator new가 막힌 자리에는 엔진이 대신 쓸 함수를 제공합니다.
UObject를 복사할 수 없습니다. 대신에 엔진은 DuplicateObject를 제공합니다. DuplicateObject는 원본 객체와 새 Outer, 새 이름을 받아서 복사한 객체를 반환합니다.UObject를 new로 생성할 수 없습니다. 대신에 엔진은 NewObject를 제공합니다. NewObject는 메모리만 할당하지 않습니다. 객체의 Outer와 이름, 플래그를 정하고, 객체의 주소를 엔진에 등록하고, 초기화에 쓸 템플릿을 넘겨서 생성자를 호출합니다. UObject의 생성자는 이 준비가 끝난 상태에서만 동작하므로, NewObject 없이 생성하면 생성자가 "is not being constructed with NewObject"라는 메시지로 Fatal 오류를 냅니다.이동 생성자가 막힌 자리에는 엔진이 대신 제공하는 함수가 없습니다. 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가 이 배열을 알지 못해서, 배열이 가리키는 객체를 의도치 않게 해제할 수 있습니다.
이번 글에서 확인한 내용은 다음과 같습니다.
UObject의 복사 생성자와 이동 생성자는 UHT가 .generated.h에 써넣는 = delete 선언으로 삭제됩니다. Epic 직원은 UObject가 값 타입으로 만든 클래스가 아니라고 답했습니다.operator new가 막혀 있어서 UObject는 복사할 수 없고, 이동할 수 없고, new로 생성할 수 없습니다. 엔진은 복사본에 DuplicateObject, 생성에 NewObject를 제공합니다. 값 타입이 이동 생성자로 하던 일은 UObject에서 포인터 값을 복사하는 것으로 충분하고, 포인터를 복사해서는 두 번째 객체가 생기지 않습니다. TArray에는 UObject 대신 포인터를 담고, 멤버 변수로 선언할 때는 UPROPERTY()를 붙입니다.다음 글에서는 블루프린트에 노출하는 함수에서 rvalue 참조와 스마트 포인터가 거절되는 이유와, 블루프린트가 넘긴 구조체가 C++ 함수에 전달되는 방식을 확인합니다.
다음 글로 바로가기 : 이동 의미론 : 8. 언리얼 엔진의 구현 - 블루프린트 호출
E-mail : [email protected] Phone number : (+82) 010 3902 8624