[언리얼엔진] 코딩 표준
- ⭐ Game Programming/Unreal Document
- 2022. 10. 27.
언리얼의 코딩 표준 및 규칙을 정리해보자
코딩 규칙이 중요한 이유
- 소프트웨어의 총 수명 비용 중 80%는 유지보수에 소모된다.
- 최초 작성자가 그 소프트웨어의 수명이 다할 때까지 유지보수하는 경우는 거의 없다.
- 코딩 규칙을 사용하면 소프트웨어의 가독성을 향상하여 엔지니어가 새로운 코드를 더 빠르고 완벽히 이해할 수 있다.
- 에픽에서 소스 코드를 모드 개발자 커뮤니티에 공개할 경우 코딩 규칙을 알고 있으면 이해하기 더 쉽다.
- 대다수의 코딩 규칙이 크로스 컴파일러 호환성에 필요하다.
클래스 채계
public을 먼저 선언하고 그 후 private 구현이 뒤따르도록 한다
저작권 공지
에픽이 배포용으로 제공한 소스 파일은 반드시 파일의 첫줄에 저작권 공지를 포함시켜야 한다.
// Copyright Epic Games, Inc. All Rights Reserved.
작명 규칙
- 영어 철자법과 문법을 사용
- 이름 내 각 단어의 첫 글자는 대문자, 단어사이에 밑줄은 없음
- 유형 이름은 대문자 한글자로 나타내는 접두사를 붙인다.
- 템플릿 클래스 접두사는 T
- UObject 상속받는 클래스의 접두사는 U
- AActor 상속받는 클래스의 접두사는 A
- SWidget 상속받는 클래스의 접두사는 S
- 추상 인터페이스의 클래스 접두사는 I
- Enum 의 접두사는 E
- Boolean 변수의 접두사는 b
- 그 외 대부분은 F
- 유형과 변수명은 명사
- 메서드 이름은 동사로 그 메서드가 하는 일이나 하는 일이 없는 경우 반환값을 설명
포팅 가능한 C++ 코드
- bool - bool 크기 추정 금지 BOOL 은 컴파일 되지 않음
- TCHAR - character TCHAR 크기 추정 금지
- uint8 - unsigned byte (1 바이트)
- int8 - signed byte (1 바이트)
- uint16 - unsigned "short"(2 바이트)
- int16 - signed "short"(2 바이트)
- uint32 - unsigned int(4 바이트)
- int32 - signed int(4 바이트)
- uint64 - unsigned "quad word"(8 바이트)
- int64 - signed "quad word"(8 바이트)
- float - single precision floating point(4 바이트)
- double - double precision floating point(8 바이트)
- PTRINT - 포인터를 가질 수 있는 integer(PTRINT 크기 추정 금지)
표준 라이브러리 사용
이전에는 표준 라이브러리를 직접 사용하는 것을 지양했지만, 최근 표준 라이브러리의 안정성과 완성도가 높아져 사용하게 되었음 자세한 예시는 문서 참조
코멘트
지침
- 자체적으로 설명이 되는 코드를 작성
// Bad:
t = s + l - b;
// 좋아요:
TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves;
- 도움이 되는 코멘트를 작성
// Bad:
// Leaves 증가
++Leaves;
// 좋아요:
// 찻잎이 더 있다는 것을 알았습니다.
++Leaves;
- 나쁜코드에는 코멘트를 작성하지 말고 그냥 새로 작성하자
// Bad:
// 잎의 총 개수는
// 작은 잎과 큰 잎을 더한 것에서
// 둘 다인 것을 뺀 것입니다.
t = s + l - b;
// Good:
TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves;
- 코드를 모순되게 만들지 말자
// Bad:
// Leaves 절대 증가 아님!
++Leaves;
// Good:
// 찻잎이 더 있다는 것을 알았습니다.
++Leaves;
Const 정확도
- 함수 인수가 함수에 의해 수정되지 않아 함수 인수를 const 포인터 또는 레퍼런스 전달하는 경우
- 메서드가 오브젝트를 수정하지 않아 const 플래그를 붙이는 경우
- 루프에서 컨테이너 자체에 대한 수정을 하지 않아 const를 사용하여 컨테이너에 반복작업을 하는 경우
최신 C++ 문법
static_assert
- 컴파일 시간 어서트가 필요한 경우에 사용
override 및 final
- 사용을 강력히 권장
nullptr
모든 경우에서 NULL 대신 nullptr 을 사용해야 한다.
'auto' 키워드
- 변수에 람다를 바인딩해야 하는 경우, 람다 유형은 코드로 표현 가능하지 않기 때문
- 이터레이터 변수의 경우. 이터레이터 유형이 매우 장황하여 가독성에 영향을 미치는 경우에 한함
- 템플릿 코드에서 표현식의 유형을 쉽게 식별할 수 없는 경우
범위 기반 for
TMap<FString, int32> MyMap;
// 옛 스타일
for (auto It = MyMap.CreateIterator(); It; ++It)
{
UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), It.Key(), *It.Value());
}
// 새 스타일
for (TPair<FString, int32>& Kvp : MyMap)
{
UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), *Kvp.Key, Kvp.Value);
}
// 옛 스타일
for (TFieldIterator<UProperty> PropertyIt(InStruct, EFieldIteratorFlags::IncludeSuper); PropertyIt; ++PropertyIt)
{
UProperty* Property = *PropertyIt;
UE_LOG(LogCategory, Log, TEXT("Property name: %s"), *Property->GetName());
}
// 새 스타일
for (UProperty* Property : TFieldRange<UProperty>(InStruct, EFieldIteratorFlags::IncludeSuper))
{
UE_LOG(LogCategory, Log, TEXT("Property name: %s"), *Property->GetName());
}
람다 및 무명 함수
람다는 자유롭게 사용 가능 스테이트풀 람다는 많이 사용하는 경향이 있는 함수 포인터에 할당할 수 없음. 자동 캡처보다 수동캡처가 좋다
- this 포인터를 포함하여 포인터 참조 캡처와 값 캡처가 때때로 허상(dangling) 참조가 발생할수 있는데, 람다 실행이 유예(deferred) 된 경우이다.
- 값 캡처는 유예식이 아닌 람다에 불필요한 사본을 만드는 경우 퍼포먼스에 우려가 있다.
- 잘못 캡처된 UObject 포인터는 가비지 콜렉터에 보이지 않는다. 자동 캡처는 멤버 변수가 참조된 경우, 묵시적으로 this를 캡처한다.
Enum 유형
Enum 클래스는 예전 네임 스페이스 enum 을 대체하여 사용해야 한다. UPROPERTY로도 지원이 되며 바이트 뿐 아니라 다른 크기로도 될수 있다. 하지만 블루프린트에 노출되는 enum은 uint8 기반이다. 플래그로 사용되는 Enum 클래스는 ENUM_CLASS_FLAGS(EnumType) 매크로를 이용하여 비트단위 연산자 전부를 자동 정의 할 수 있다.
// 옛 enum
UENUM()
namespace EThing
{
enum Type
{
Thing1,
Thing2
};
}
// 새 enum
UENUM()
enum class EThing : uint8
{
Thing1,
Thing2
}
// 옛 프로퍼티
UPROPERTY()
TEnumAsByte<EThing::Type> MyProperty;
// 새 프로퍼티
UPROPERTY()
EThing MyProperty;
enum class EFlags
{
None = 0x00,
Flag1 = 0x01,
Flag2 = 0x02,
Flag3 = 0x04
};
ENUM_CLASS_FLAGS(EFlags)
이동 시멘틱
TArray, TMap, TSet, FString 에는 move 생성자와 move 할당 연산자가 있다. MoveTemp를 통해 명시적으로 호출 가능.
디폴트 멤버 이니셜라이저
클래스 자체 내 클래스 기본값을 정의하는 데 사용
UCLASS()
class UTeaOptions : public UObject
{
GENERATED_BODY()
public:
UPROPERTY()
int32 MaximumNumberOfCupsPerDay = 10;
UPROPERTY()
float CupWidth = 11.5f;
UPROPERTY()
FString TeaType = TEXT("Earl Grey");
UPROPERTY()
EDrinkingStyle DrinkingStyle = EDrinkingStyle::PinkyExtended;
};
장점
- 여러 생성자에 걸쳐 이니셜라이저를 복제할 필요가 없음
- 초기화 순서와 선언 순서가 섞일 일이 없음
- 멤버 유형, 프로퍼티 플래그, 기본값이 모두 한 곳에 있어, 가독성과 유지보수성에 좋다
단점
- 기본값을 변경하면 모든 종속 파일을 리빌드해야 함
- 헤더는 엔진 패치 릴리즈에서 변경할 수 없으므로, 가능한 픽스 종류가 제한될 수 있다
- 이런 방식으로 초기화시킬 수는 없는 것들도 있다. 예로 베이스 클래스, UObject 서브오브젝트, 앞서 선언한(forward-declared) 유형으로의 포인터, 컨스트럭터 인수에서 추론해 낸 값, 여러 단계에 걸쳐 초기화된 멤버 등
- 헤더에 이니셜라이저를 조금 두고, 나머지는 .cpp 파일의 생성자에 두게 되면 가독성과 유지보수성에 좋지 않을 수 있다.
서드 파티 코드
엔진에서 사용하는 라이브러리에 코드를 수정할 때 마다, 변경 내용에 왜 변경했는지에 대한 설명이 되는 태그를 달자.
// @third party code - BEGIN PhysX
#include <physx.h>
// @third party code - END PhysX
// @third party code - BEGIN MSDN SetThreadName
// [http://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx]
// 디버거에서 스레드 이름을 설정하는 데 사용됨
...
//@third party code - END MSDN SetThreadName
코드 포맷
중괄호
에픽에서는 새 줄에 중괄호를 넣는다.
if (bThing)
{
return;
}
if - else
if-else 문의 각 실행 블록은 중괄호로 묵어야 한다.
if (bHaveUnrealLicense)
{
InsertYourGameHere();
}
else
{
CallMarkRein();
}
if (TannicAcid < 10)
{
UE_LOG(LogCategory, Log, TEXT("Low Acid"));
}
else if (TannicAcid < 100)
{
UE_LOG(LogCategory, Log, TEXT("Medium Acid"));
}
else
{
UE_LOG(LogCategory, Log, TEXT("High Acid"));
}
탭 및 들여쓰기
- 실행 블록별로 코드를 들여쓰자
- 줄 시작부분의 공백은 스페이스가 아니라 탭을 사용. 탭 크기는 4자로 설정.
- C#로 코드를 작성한다면 스페이스가 아니라 탭을 사용.
Switch 문
switch case 문에서는 다음 케이스로 넘어가는지를 명시적으로 밝혀야 함. 각각의 case 마다 break를 넣던다. fall through 코멘트를 달자. return, continue 등도 괜찮음. default case는 항상 만들어야 한다.
switch (condition)
{
case 1:
...
// falls through
case 2:
...
break;
case 3:
...
return;
case 4:
case 5:
...
break;
default:
break;
}
네임스페이스
- 대부분의 UE 코드는 현재 글로벌 네임스페이스에 둘러싸여 있지 않음. 전역 범위에서 충돌이 일어나지 않도록 주의
- 언리얼 헤더 툴에는 네임스페이스가 지원되지 않으므로, UCLASS , USTRUCT 등을 정의할 때는 사용할 수 없다.
- UCLASSes , USTRUCTs 등이 아닌 새 API는 적어도 UE:: 네임스페이스에 배치해야 하며 이상적으로는 중첩된 네임스페이스(예: UE::Audio:: )를 사용하는 것이 좋다. 누구에게나 공개되는 API의 일부가 아닌 구현 세부 정보 포함에 사용되는 네임스페이스는 Private 네임스페이스(예: UE::Audio::Private:: )에 들어가야 한다.
- Using 선언:
- 전역 범위에는 using 선언을, .cpp 파일에서도 넣지 말자('unity' 빌드 시스템에 문제가 발생)
- 다른 네임스페이스 안이나 함수 바디 안에는 using 선언을 넣어도 괜찮음
- 네임스페이스 안에 using 선언을 넣는 경우, 해당 네임스페이스 동일 변환 단위 내 다른 곳으로 이어지게 된다.
- 오직 위 규칙을 따를 때만 헤더 파일에서 using 선언을 사용해도 안전
- 참고로 앞서 선언된 유형은 각각의 네임스페이스 안에서 선언해야 함. 그렇게 하지 않으면 링크 오류가 발생
- 한 네임스페이스 안에 다수의 클래스/유형을 선언하면, 다른 전역 범위의 클래스에서 사용하기가 어려울 수 있다(예를 들면, 함수 시그니처는 클래스 선언에 나타날 때 명시적 네임스페이스를 사용해야 한다)
- using 선언을 사용해서 네임스페이스 안의 특정 변수만 자신의 범위로 에일리어싱할 수 있다(예: using Foo:FBar). 그러나 언리얼 코드에서는 권장하지 않음.
- 매크로는 네임스페이스 내에 있을 수 없지만, 대신 UE_ 접두사를 붙이자(예: UE_LOG ).
물리적 종속성
- 파일 이름에는 가급적 접두사를 붙이지 말자. 예를 들면 UScene.cpp 보다는 Scene.cpp. 그래야 Workspace Whiz나 Visual Assist같은 툴에서 Open File in Solution 같은 기능을 사용할 때, 원하는 파일을 명확히 구분해 내는 데 필요한 글자 수를 줄이는 등 사용하기가 용이하다.
- 모든 헤더는 #pragma once 디렉티브(지시자)로 복수의 include를 방지해야 된다. 참고로 요즘의 컴파일러는 전부 #pragma once 를 지원.
- 일반적으로는 물리적 결합을 최소화시키자. 특히, 다른 헤더의 표준 라이브러리 헤더를 include하지 말자.
- 헤더 include 대신 앞선 선언이 가능하면 그렇게 하자.
- include할 때는 가급적 세세하게 하자. 예를 들어 Core.h를 include하지 말고, Core의 헤더 중 정의가 필요한 특정 부분을 include하자.
- 세세한 include 작업을 쉽게 하기 위해, 필요한 헤더는 전부 직접 include 하자
- 자신이 include한 다른 헤더에 의해 간접적으로 include되는 헤더에 의존하지 말자
- 다른 헤더를 통해 include시키기 보다는, 필요한 것을 전부 include 하자
- 모듈에는 Private과 Public 소스 디렉터리가 있다. 다른 모듈이 필요로 하는 정의는 Public 디렉터리의 헤더에 있어야 한다. 그 외 모든 것은 Private 디렉터리에 있어야 한다. 참고로 구형 언리얼 모듈의 경우 이 디렉터리는 'Src' 및 'Inc'로 불리기도 하는데, 이름만 그럴 뿐 같은 방식으로 프라이빗 코드와 퍼블릭 코드를 구분하기 위함일 뿐이지, 헤더 파일을 소스 파일과 구분하기 위함은 아니다.
- 미리 컴파일된 헤더 생성용 헤더 설정은 UnrealBuildTool이 알아서 잘한다.
- 큰 함수는 논리적 하위 함수로 나눈다. 컴파일러 최적화 중 한 분야가 공통 하위 표현식 삭제다. 함수가 클 수록 그 식별을 위해 컴파일러가 할 일이 많아지고, 빌드 시간이 크게 늘어나게 된다.
- 인라인 함수는 너무 많이 사용하지 말자. 사용하지 않는 파일에 있어도 강제로 리빌드시키기 때문, 인라인 함수는 사소한 접근자에만, 또는 프로파일링을 통해 이득이 있는 것으로 보일때만 사용해야 한다.
- FORCEINLINE 사용에 있어서는 조금 더 보수적이어야 한다. 모든 코드와 로컬 변수는 호출하는 함수로 확장되어, 큰 함수에서 발생하는 것과 동일한 빌드 시간 문제가 생긴다.
캡슐화
클래스 멤버는 클래스의 인터페이스 일부가 아닌 한 거의 항상 private로 선언 하자. 더 이상 파생시킬 클래스가 아닌경우 final을 사용하자
일반적인 스타일 문제
- 종속성 거리를 최소화하자. 코드가 특정 값을 갖는 변수에 의존할 때는, 변수를 사용하기 직전에 그 값을 설정해두자. 실행 블록 상단에 변수 값을 초기설정해 둔 상태로 코드 수백 줄 동안 사용하지 않는다면, 그 종속성을 모르는 사람이 실수로 그 값을 바꾸게 될 여지가 많이 있다. 바로 다음 줄에 사용한다면 변수 초기설정을 왜 그렇게 했는지, 어디서 사용되는지를 명확히 알 수 있다.
- 메서드는 가급적 하위 메서드로 분할하자. 세밀한 부분부터 시작해서 큰 그림을 재구성하기보다는, 큰 그림을 먼저 그린 후 흥미를 끄는 세밀한 부분으로 파 내려가는 것이 더 쉽다. 마찬가지로, 모든 코드가 통째로 들어 있는 메서드 보다는, 이름을 잘 지어둔 다수의 하위 메서드를 연속적으로 호출하는 단순한 메서드를 이해하기가 수월하다.
- 함수 선언이나 함수 호출 위치에서 함수의 이름과 인수 목록에 선행되는 괄호 사이에 스페이스를 추가하지 말자.
- 컴파일러 경고에 주의를 기울이자. 컴파일러 경고도 고쳐야 한다. 전혀 처리할 수가 없다면 #pragma 로 억제시킬 수는 있지만, 이는 정말 마지막 수단이어야 한다.
- 파일 끝에 빈 줄을 하나 놔두자. 모든 .cpp 와 .h 파일은 빈 줄이 있어야 gcc에 제대로 돌아간다.
- 디버그 코드는 일반적으로 유용하고 잘 다듬어진 상태가 아니라면 체크인하지 말자. 디버그 코드가 다른 코드와 섞이면 다른 코드를 읽기가 힘들어 지기때문
- 스트링 리터럴 주변에는 항상 TEXT() 매크로를 사용. 그러지 않으면, 코드가 리터럴에서 FString 을 생성하는 경우 원치 않는 스트링 변환 프로세스가 유발.
- 루프에서의 동일 연산 반복을 피하자. 공통된 하위 표현식은 루프 밖으로 빼서 중복 계산을 피하자. 어떤 경우에는 statics를 활용하여 전역 범위에서의 함수 호출을 대상으로 하는 중복 연산을 피할 수 있는데, 스트링 리터럴에서의 FName 생성같은 경우를 예로 들 수 있다.
- 핫 리로드 기능을 염두하자. 종족성을 최소화시켜 반복작업 시간을 줄인다. 리로드 동안에 변할 확률이 있는 함수에는 인라인 또는 템플릿을 사용하지 말자. 리로드 동안 그대로 남아있을 듯한 것에만 statics를 사용하도록
- 복잡한 표현식은 중간 변수를 사용하여 단순화 시켜야 한다. 복잡한 표현식을 중간 변수에 할당된 하위 표현식으로 나누고, 부모 표현식 내에서 하위 표현식의 의미를 설명하는 이름을 지정하면 이해하기가 수월해진다.
//이전 if ((Blah->BlahP->WindowExists->Etc && Stuff) && !(bPlayerExists && bGameStarted && bPlayerStillHasPawn && IsTuesday()))) { DoSomething(); }
//변경 후
const bool bIsLegalWindow = Blah->BlahP->WindowExists->Etc && Stuff;
const bool bIsPlayerDead = bPlayerExists && bGameStarted && bPlayerStillHasPawn && IsTuesday();
if (bIsLegalWindow && !bIsPlayerDead)
{
DoSomething();
}
- 포인터와 레퍼런스의 스페이스는 그 오른쪽에 딱 한 칸만 둬야 한다. 그래야 특정 유형에 대한 모든 포인터나 레퍼런스를 빠르게 Find in Files 할 수 있다.
```cpp
FShaderType* Ptr //OK
FShaderType *Ptr //NO
FShaderType * Ptr //NO
- 변수 음영(shadowed)은 허용되지 않는다.
class FSomeClass
{
public:
void Func(const int32 Count)
{
for (int32 Count = 0; Count != 10; ++Count)
{
// Use Count
}
}
private:
int32 Count;
}
- 함수 호출에서 익명 리터럴 사용은 피하자
// 옛 스타일
Trigger(TEXT("Soldier"), 5, true);.
// 새 스타일
const FName ObjectName = TEXT("Soldier");
const float CooldownInSeconds = 5;
const bool bVulnerableDuringCooldown = true;
Trigger(ObjectName, CooldownInSeconds, bVulnerableDuringCooldown);
API 디자인 지침
- bool 함수 파라미터는 피해야 하며, 함수에 전달되는 플래그의 경우 특히 그렇다. 앞서 언급한 익명 리터럴 문제가 그대로 발생하며, 시간에 따라 API 확장을 통해 동작이 추가되면서 늘어나는 경향도 있다. 대신 enum 사용을 권장(강 유형 enum 섹션의 enum 을 플래그로 사용하는 데 대한 조언을 참고):
// 옛 스타일
FCup* MakeCupOfTea(FTea* Tea, bool bAddSugar = false, bool bAddMilk = false, bool bAddHoney = false, bool bAddLemon = false);
FCup* Cup = MakeCupOfTea(Tea, false, true, true);
// 새 스타일
enum class ETeaFlags
{
None,
Milk = 0x01,
Sugar = 0x02,
Honey = 0x04,
Lemon = 0x08
};
ENUM_CLASS_FLAGS(ETeaFlags)
FCup* MakeCupOfTea(FTea* Tea, ETeaFlags Flags = ETeaFlags::None);
FCup* Cup = MakeCupOfTea(Tea, ETeaFlags::Milk | ETeaFlags::Honey);
- 너무 긴 함수 파라미터 리스트는 피하자. 함수가 파라미터를 많이 받는 경우 전용 구조체 전달을 고려하자
// 옛 스타일
TUniquePtr<FCup[]> MakeTeaForParty(const FTeaFlags* TeaPreferences, uint32 NumCupsToMake, FKettle* Kettle, ETeaType TeaType = ETeaType::EnglishBreakfast, float BrewingTimeInSeconds = 120.0f);
// 새 스타일
struct FTeaPartyParams
{
const FTeaFlags* TeaPreferences = nullptr;
uint32 NumCupsToMake = 0;
FKettle* Kettle = nullptr;
ETeaType TeaType = ETeaType::EnglishBreakfast;
float BrewingTimeInSeconds = 120.0f;
};
TUniquePtr<FCup[]> MakeTeaForParty(const FTeaPartyParams& Params);
- bool 및 FString 을 사용한 함수 오버로드는 피해야한다. 작동 방식을 예상할 수 없다.
void Func(const FString& String);
void Func(bool bBool);
Func(TEXT("String")); // bool 오버로드 호출!
- 인터페이스(접두사가 'I' 인) 클래스는 항상 추상형이어야 하며, 멤버 변수가 있어서는 안 된다. 인터페이스는 순수 가상이 아닌 메서드를 포함할 수 있으며, 심지어 인라인 구현되는 한 가상이 아니거나 정적인 메서드도 포함할 수 있다.
- 오버라이딩 메서드를 선언할 때는 virtual 및 override 키워드를 사용. 파생 클래스에서 가상 함수를 선언할 때, 그 클래스가 부모 클래스에서 가상 함수를 오버라이드할 때는, virtual 및 override 키워드 둘 다 사용해야 한다. 예:
class A
{
public:
virtual void F() {}
};
class B : public A
{
public:
virtual void F() override;
}
플랫폼별 코드
플랫폼별 코드는 항상 적합한 이름의 하위 디렉터리 아래 플랫폼별 소스 파일에 추상화 및 구현해야 함
Source/Runtime/Core/Private/[PLATFORM]/[PLATFORM]Memory.cpp
일반적으로 [PLATFORM] 이름의 디렉터리 밖에서 코딩하려면 (PLATFORM_XBOXONE 같은) PLATFORM_[PLATFORM] 형태는 피해야 한다.
그 대신, 하드웨어 추상 층을 확장해서 static 함수를 추가하자. 예를 들어 FPlatformMisc의 경우:
FORCEINLINE static int32 GetMaxPathLength()
{
return 128;
}
그런 다음 플랫폼에서 이 함수를 오버라이드하여 플랫폼별 상수 값을 반환하거나 심지어 플랫폼 API를 사용해서 결과를 알아낼 수도 있다.
함수에 포스 인라인을 사용하면 디파인을 사용하는 것과 똑같은 퍼포먼스 특징이 생긴다.
디파인이 반드시 필요한 경우, #define을 새로 만들어 플랫폼에 적용되는 특정 프로퍼티를 설명하자.
(예: PLATFORM_USE_PTHREADS . Platform.h에 기본값을 설정해 두고, 필요한 플랫폼에서 그 플랫폼별 Platform.h 파일에 오버라이드)
예를 들어 Platform.h에는:
#ifndef PLATFORM_USE_PTHREADS
#define PLATFORM_USE_PTHREADS 1
#endif
Windows/WindowsPlatform.h 에는:
#define PLATFORM_USE_PTHREADS 0
이렇게 있다면, 크래스 플랫폼 코드는 플랫폼을 알 필요 없이 디파인을 사용하면 된다.
#if PLATFORM_USE_PTHREADS
#include "HAL/PThreadRunnableThread.h"
#endif
문서
https://docs.unrealengine.com/4.27/ko/ProductionPipelines/DevelopmentSetup/CodingStandard/