블루프린트가 C++ 함수를 호출할 때 매개변수 선언별로 복사와 이동이 몇 번 일어나는지 알아봅니다.


돌아가기

프로그래밍 언어

방정현

목차

이동 의미론 : 8. 언리얼 엔진의 구현 - 블루프린트 호출

개요

7편에서 UObject는 복사와 이동이 막혀 있어서 포인터로 다룬다는 것을 확인했습니다. USTRUCT로 선언한 구조체는 UObject와 달리 복사 생성자와 이동 생성자가 삭제되지 않습니다. 그래서 C++ 함수가 다른 C++ 함수를 호출할 때는 인자에 MoveTemp를 붙여서 구조체를 복사하지 않고 이동할 수 있습니다. MoveTemp는 5편에서 확인한 언리얼 엔진의 함수 템플릿이고, 표준 C++의 std::move와 같은 일을 합니다. 이번 글에서는 블루프린트가 C++ 함수를 호출할 때도 구조체를 이동할 수 있는지 엔진 소스로 확인해 보겠습니다.

검증 엔진은 UE 5.7.0입니다. 복사와 이동 횟수는 UE 5.8.2에서 측정했습니다.


1. 값 전달

매개변수를 값으로 선언한 C++ 함수에서는, 호출하는 코드가 인자에 MoveTemp를 붙였는지에 따라 매개변수를 생성하는 생성자가 달라집니다.

void ApplyData(FMyData Data);

FMyData A;
ApplyData(A);             // 매개변수 Data가 복사 생성자로 생성됩니다.
ApplyData(MoveTemp(A));   // 매개변수 Data가 이동 생성자로 생성됩니다.

이 함수에 UFUNCTION(BlueprintCallable)을 붙이면 블루프린트에서 호출할 수 있습니다. 블루프린트에 FMyData 타입의 멤버 변수 MyVar가 있고, 블루프린트 그래프에서 ApplyData 노드의 Data 핀에 MyVar를 연결했다고 하겠습니다.

image.png

블루프린트는 C++ 함수 ApplyData를 직접 호출하지 않습니다. 블루프린트 바이트코드를 실행하는 블루프린트 가상 머신(Virtual Machine, VM)이 UHT가 .gen.cpp에 생성한 중간 함수 execApplyData를 호출하고, execApplyData가 ApplyData를 호출합니다. 따라서 MyVar가 ApplyData의 매개변수 Data로 복사되는지 이동되는지는 execApplyData의 코드에 달려 있습니다.

//UHT가 MyObject.gen.cpp에 생성하는 중간 함수

DEFINE_FUNCTION(UMyObject::execApplyData)
{
	P_GET_STRUCT(FMyData,Z_Param_Data);    // (A)
	P_FINISH;
	P_NATIVE_BEGIN;
	P_THIS->ApplyData(Z_Param_Data);       // (B)
	P_NATIVE_END;
}

P_GET_STRUCT와 P_THIS는 매크로입니다. 위 코드의 (A)와 (B)를 펼치면 다음과 같습니다.

//Engine/Source/Runtime/CoreUObject/Public/UObject/ScriptMacros.h (31 ~ 33, 68, 143 ~ 145)의 매크로를 펼친 모습

// (A)를 펼친 두 줄
// (1) execApplyData가 지역 변수 Z_Param_Data를 기본 생성자로 생성합니다.
FMyData Z_Param_Data;
// (2) 복사: 블루프린트 VM이 MyVar의 값을 Z_Param_Data에 복사 대입합니다.
Stack.StepCompiledIn<FStructProperty>(&Z_Param_Data);

// (B)를 펼친 한 줄
// (3) 복사: MoveTemp가 붙지 않았으므로 매개변수 Data가 Z_Param_Data로부터 복사 생성됩니다.
((UMyObject*)Context)->ApplyData(Z_Param_Data);

매개변수를 값으로 선언한 함수를 블루프린트가 호출하면 FMyData는 (2)에서 복사 대입으로 한 번, (3)에서 복사 생성으로 한 번, 모두 두 번 복사됩니다. FMyData에 이동 생성자가 있어도 이동 생성자는 호출되지 않습니다.

매개변수를 rvalue 참조(FMyData&&)로 선언하는 방법도 쓸 수 없습니다. UFUNCTION의 매개변수에 &&를 쓰면 UHT가 "Found '&&' when expecting end of type declaration"이라는 에러를 내고 빌드가 실패합니다.


2. const 참조 전달

이번에는 매개변수를 const 참조로 선언합니다. 블루프린트 그래프에서는 1절과 같이 SetData 노드의 Data 핀에 MyVar를 연결합니다.

UFUNCTION(BlueprintCallable)
void SetData(const FMyData& Data);

image.png

UHT는 SetData의 중간 함수 execSetData에 P_GET_STRUCT 대신 P_GET_STRUCT_REF를 씁니다. execSetData의 매크로를 펼치면 다음과 같습니다.

//Engine/Source/Runtime/CoreUObject/Public/UObject/ScriptMacros.h (43 ~ 45, 69, 143 ~ 145)의 매크로를 펼친 모습

// (1) execSetData가 지역 변수 Z_Param_Out_DataTemp를 기본 생성자로 생성합니다.
FMyData Z_Param_Out_DataTemp;
// (2) 복사: StepCompiledInRef 안에서 블루프린트 VM이 MyVar의 값을 Z_Param_Out_DataTemp에 복사 대입합니다.
//     복사 없음: StepCompiledInRef는 MyVar의 참조를 반환하고, 참조(FMyData&)인 Z_Param_Out_Data가 그 참조를 받습니다.
FMyData& Z_Param_Out_Data = Stack.StepCompiledInRef<FStructProperty, FMyData>(&Z_Param_Out_DataTemp);
// (3) 복사 없음: 매개변수 Data가 MyVar를 참조합니다.
((UMyObject*)Context)->SetData(Z_Param_Out_Data);

매개변수를 const 참조로 선언한 함수를 블루프린트가 호출하면 FMyData는 (2)에서 한 번 복사됩니다. 복사본 Z_Param_Out_DataTemp는 SetData에 전달되지 않고, 그 뒤에 쓰이지 않습니다. 값으로 선언했을 때보다 복사가 한 번 적으므로, 블루프린트가 호출하는 함수가 구조체를 읽기만 한다면 매개변수를 const 참조로 선언합니다.


3. 반환값

C++ 함수가 구조체를 블루프린트에 반환하는 방법은 두 가지입니다. 반환값으로 반환하는 방법과 출력 매개변수에 대입하는 방법입니다.

UFUNCTION(BlueprintCallable)
FMyData GetData();                  // 반환값

UFUNCTION(BlueprintCallable)
void FillData(FMyData& OutData);    // 출력 매개변수

void UMyObject::FillData(FMyData& OutData)
{
	FMyData Local;
	OutData = MoveTemp(Local);
}

두 함수의 중간 함수 execGetData와 execFillData의 매크로를 펼치면 다음과 같습니다.

//Engine/Source/Runtime/CoreUObject/Public/UObject/ScriptMacros.h (43 ~ 45, 69, 143 ~ 145)의 매크로를 펼친 모습

// execGetData
// 이동: GetData()가 반환한 임시 객체를 블루프린트 지역 변수에 이동 대입합니다.
*(FMyData*)Z_Param__Result = ((UMyObject*)Context)->GetData();

// execFillData
FMyData Z_Param_Out_OutDataTemp;
// 복사: 블루프린트 VM이 블루프린트 지역 변수의 값을 Z_Param_Out_OutDataTemp에 복사 대입합니다.
FMyData& Z_Param_Out_OutData = Stack.StepCompiledInRef<FStructProperty, FMyData>(&Z_Param_Out_OutDataTemp);
// 이동: FillData 본문의 OutData = MoveTemp(Local); 이 이동 대입 연산자를 호출합니다.
((UMyObject*)Context)->FillData(Z_Param_Out_OutData);

Z_Param__Result는 GetData 노드의 반환값을 저장할 블루프린트 지역 변수의 주소입니다. GetData()가 반환한 값은 임시 객체(rvalue)이므로 execGetData의 대입문은 FMyData의 이동 대입 연산자를 호출합니다. 구조체를 값으로 반환하는 함수를 블루프린트가 호출하면 FMyData는 복사되지 않고 한 번 이동됩니다.

execFillData는 2절의 execSetData처럼 P_GET_STRUCT_REF를 쓰므로, 2절의 (2)와 같은 복사가 한 번 일어납니다. 출력 매개변수로 선언한 함수를 블루프린트가 호출하면 FMyData는 한 번 복사되고 한 번 이동됩니다. FillData가 MoveTemp 없이 OutData = Local;로 대입하면 이동 대신 복사가 한 번 더 일어납니다. 어느 쪽이든 출력 매개변수는 반환값보다 복사가 많으므로, 복사를 줄이려고 반환값을 출력 매개변수로 바꿀 필요가 없습니다.

다만, 블루프린트 그래프에서 GetData 노드의 반환값을 MyVar에 저장하면, 블루프린트 지역 변수에서 MyVar로 대입할 때 FMyData가 한 번 복사됩니다.


정리

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

블루프린트가 C++ 함수를 호출할 때는 UHT가 생성한 중간 함수가 인자를 넘깁니다. 중간 함수는 인자에 MoveTemp를 붙이지 않으므로, 구조체에 이동 생성자가 있어도 인자는 이동되지 않고 복사됩니다.

블루프린트가 각 함수를 호출했을 때 FMyData가 복사된 횟수와 이동된 횟수는 다음 표와 같습니다.

함수 선언 복사 이동
void ApplyData(FMyData Data) 2번 0번
void SetData(const FMyData& Data) 1번 0번
FMyData GetData() 0번 1번
void FillData(FMyData& OutData) 1번 1번

그래서 블루프린트에서 호출하는 함수는 다음과 같이 선언합니다.

bundle4_table.png

이 글로 이동 의미론 시리즈를 마칩니다. 1~4편에서는 표준 C++의 이동 의미론을, 5~8편에서는 언리얼 엔진이 이동을 다루는 방식을 확인했습니다.

참고


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