All is well

[오늘의에러] error C4840: variadic 함수의 인수로서 'FString' 클래스를 이식 불가능하게 사용했습니다. (feat. `UE_LOG`에서 `FString` 사용 시 에러 발생) 본문

오늘의 에러

[오늘의에러] error C4840: variadic 함수의 인수로서 'FString' 클래스를 이식 불가능하게 사용했습니다. (feat. `UE_LOG`에서 `FString` 사용 시 에러 발생)

D0YUN 2025. 3. 1. 12:44

 

오늘의 에러

TPS_GameModeBase.cpp(10): error C4840: variadic 함수의 인수로서 'FString' 클래스를 이식 불가능하게 사용했습니다.

UE에서 Custom Log를 만들고 사용하는 과정에서 다음과 같은 에러를 만났다

 


에러 발생 상황

로그 작성

Custom Log Category 및 Custom Log는 `TPSMAR.h`에서 작성하였다.

 

 

로그 사용을 위한 등록

로그 사용을 위해 `TPSMAR.cpp`에 Log Category를 등록하였고

 

 

`TPS_GameModeBase`에서 사용하였다. 

빨간색으로 표시한 부분에서 에러가 발생하였다.

 


에러 분석

TPS_GameModeBase.cpp(10): error C4840: variadic 함수의 인수로서 'FString' 클래스를 이식 불가능하게 사용했습니다.

해당 에러는 가변 인자(variadic arguments) 함수에서 FString을 직접 전달할 때 발생하는 오류라고 한다.

 


에러 원인

결론부터 말하면 ` PRINT_LOG(fmt, ...)`의 `FString::Printf`를 이용하여 포맷 문자열을 전달하는 부분이 잘못되었다.

 

`ATPS_GameModeBase` 생성자에서 위 매크로를 다음과 같이 호출했다.

PRINT_LOG(TEXT("My Log : %s"), TEXT("TPS Project!"));

 

이를 확장하면 다음과 같다.

UE_LOG(TPS, Warning, TEXT("%s %s"), *CALLINFO, FString::Printf(TEXT("My Log : %s"), TEXT("TPS Project!")));

 

문제는 `FString::Printf(TEXT("My Log : %s"), TEXT("TPS Project!"))` 부분이다.


`UE_LOG` 매크로는 내부적으로 `TCHAR*` 형을 인자로 요구한다.

그런데 `FString::Printf`는 `TCHAR*`이 아닌 `FString`을 반환한다. 

`FString`을 직접 넣으면 변환 과정에서 호환되지 않는 타입 문제가 생겨 오류가 발생하는 것이었다.

 


해결 방법 1 : `FString::Printf()`의 결과를 `*` 연산자로 변환

가장 간단한 방법이다. `FString::Printf()`의 결과를 `*` 연산자를 이용하여 `TCHAR*`로 형 변환을 진행하는 것이다.

#define PRINT_LOG(fmt, ...) UE_LOG(TPS, Warning, TEXT("%s %s"), *CALLINFO, *FString::Printf(fmt, ##__VA_ARGS__))

이렇게 수정하면 `FString::Printf()`의 결과가 `TCHAR*`이 되어 `UE_LOG`와 호환되게 된다.


해결 방법 2 : `FString::Printf() ` 없이 직접 `UE_LOG` 사용

#define PRINT_LOG(fmt, ...) UE_LOG(TPS, Warning, TEXT("%s ") fmt, *CALLINFO, ##__VA_ARGS__)

이렇게 수정하면 `FString::Printf()` 없이도 바로 사용할 수 있다.

 

그런데 `TEXT("%s ") fmt` 이 어떤 원리로 가능한 건지 궁금해졌다.

 

`TEXT("%s ") fmt` 문법이 가능한 이유

C++의 매크로 확장 문자열 포맷팅 방식을 활용하여 가능하다.

 

매크로 내에서 문자열 결합

C++ 매크로에서 문자열 리터럴을 직접 결합하면 컴파일러자동으로 하나의 문자열로 병합한다.

#define EXAMPLE "Hello " "World!"

위 코드는 컴파일 시 `EXAMPLE`이 ``"Hello World!"`로 확장된다.

 

`TEXT("%s ") fmt`을 통해 살펴보는 매크로 내 문자열 결합

#define PRINT_LOG(fmt, ...) UE_LOG(TPS, Warning, TEXT("%s ") fmt, *CALLINFO, ##__VA_ARGS__)

위 매크로를

PRINT_LOG(TEXT("My Log : %s"), TEXT("TPS Project!"));

다음과 같이 호출하면

UE_LOG(TPS, Warning, TEXT("%s My Log : %s"), *CALLINFO, TEXT("TPS Project!"));

`%s `에서 `CALLINFO`를 출력하고, 그 뒤에 사용자가 넣은 문자열 포맷(`fmt`)을 그대로 출력하는 형태로 확장된다.

 

 

즉, `UE_LOG`의 가변 인자(`##__VA_ARGS__`)와 자연스럽게 호환이 된다는 것이고, 이는 

`PRINT_LOG` 매크로 속`TEXT("%s ") fmt`이 `fmt`이 `TEXT("%s ")`와 자동으로 결합 돼 하나의 문자열로 처리되는 것을 의미한다.


성능 비교 - 방법 1 vs 방법 2

방법 1 : `FString::Printf` 활용

#define PRINT_LOG(fmt, ...) UE_LOG(TPS, Warning, TEXT("%s %s"), *CALLINFO, *FString::Printf(fmt, ##__VA_ARGS__))

 

 

방법 1의 `FString::Printf(fmt, ##__VA_ARGS__)`는 새로운 `FString` 객체를 생성하여 반환하기 때문에 `*` 연산자를 사용해야 한다.

즉, 불필요한 문자열 객체 생성이 발생하기 때문에 성능 저하 가능성이 있다.

 

 

방법 2 : `UE_LOG` 직접 활용

#define PRINT_LOG(fmt, ...) UE_LOG(TPS, Warning, TEXT("%s ") fmt, *CALLINFO, ##__VA_ARGS__)

 

 

 

방법 2는 `fmt`이 직접 확장되므로 `FString::Printf()`를 사용할 필요가 없고, `UE_LOG`의 가변 인자를 그대로 활용하므로 더 효율적이다.

 

 


 

 

해결 완료

`UE_LOG`는 인자로 `TCHAR*`를 받는다는 것 명심하자!