언리얼 코딩 표준
🍋 에픽 코딩 표준
코딩 규칙이 프로그래머에게 중요한 이유는 다음과 같다.
- 소프트웨어의 총 수명 비용 중 80%는 유지보수에 소모된다.
- 최초 작성자가 그 소프트웨어의 수명이 다할 때까지 유지보수하는 경우는 거의 없다.
- 코딩 규칙을 사용하면 소프트웨어의 가독성을 향상하여 엔지니어가 새로운 코드를 더 빠르고 완벽히 이해할 수 있다.
- 에픽에서 소스 코드를 모드 개발자 커뮤니티에 공개할 경우 코딩 규칙을 알고 있으면 이해하기 더 쉽다.
- 대다수의 코딩 규칙이 크로스 컴파일러 호환성에 필요하다.
다음의 코딩 표준은 C++ 중심이지만, 코딩 표준의 핵심은 어떤 언어를 사용하든 관계없이 따라야 한다. 해당할 경우 특정 언어에 대한 규칙이나 예외 사항을 다룬 섹션을 제공할 수 있다.
클래스 체계
클래스는 작성자보다는 읽는 사람을 염두에 두고 조직되어야 한다. 읽는 사람은 대부분 클래스의 퍼블릭 인터페이스를 사용할 것이므로, 퍼블릭 인터페이스에서 먼저 선언한 후 클래스의 프라이빗 구현이 뒤따라야한다.
명명 규칙
다양한 명명 규칙이 존재한다.
- 파스칼: 합성어의 첫 글자를 대문자를 사용해 명명.
- 카멜: 첫 합성어는 소문자로 나머지는 대문자를 사용해 명명.
- 스네이크: 합성어 사이에 언더바를 사용해 명명.
// pascal
int UnrealEngine;
// camel
int unrealEngine;
// snake
int unreal_engine;
- 탬플릿 클래스에는 접두사 T를 포함한다.
UObject
에서 상속받는 클래스에는 접두사U
를 포함한다.AActor
에서 상속받는 클래스에는 접두사A
를 포함한다.SWidget
에서 상속받는 클래스에는 접두사S
를 포함한다.- 추상적 인터페이스인 클래스에는 접두사
I
를 포함한다. - 열거형은 접두사
E
를 포함한다. - 부울 변수는 접두사
b
를 포함한다. - 구조체와 일반적인 c++ 클래스들은
F
를 포함한다.
언리얼은 파스칼 규칙을 사용한다.
// 부울을 반환하는 함수는 질의형태로 작성해야 한다.
bool IsVisible();
bool ShouldClearBuffer();
// 프로시저(void리턴형)는 강한 동사 뒤에 오브젝트를 붙여 써야 한다.
// 메서드의 오브젝트가 그 안에 있는 오브젝트 일 때는 예외이며, 이 경우 오브젝트는 컨텍스트에서 이해된다.
// Handle 및 Process 등의 모호한 동사로 시작하는 이름은 피해야 한다.
필수 사항은 아니지만, 함수 파라미터가 참조로 전달된 후 함수가 그 값을 쓸 것으로 예상되는 경우 이름 앞에 접두사 ‘Out’을 추가할 것을 권장한다. 이렇게 하면 이 아규먼트에 전달되는 값이 함수로 대체된다는 것을 확실히 알 수 있다.
In 또는 Out 파라미터도 부울인 경우 bOutResult와 같이 In/Out 접두사 앞에 b
를 붙힌다.
값을 반환하는 함수는 변환 값을 설명해야 한다. 함수가 어떤 값을 변환하는 지 이름을 보고 정확히 알 수 있어야 한다. 특히 부울 함수의 경우 이는 매우 중요하다. 예시로 다음 두가지 방법을 확인.
// true일 경우 무슨 의미인지 알 수 있나??
bool CheckTea(FTea tea);
// 이름을 통해 true일 경우 차가 신선하다는 것을 명확히 알 수 있다.
bool IsTeaFresh(FTea tea)
포터블 C++ 코드
bool
- 부울 값(부울 크기 추정 금지).BOOL
은 컴파일되지 않는다.TCHAR
- character(문자) (TCHAR 크기 추정 금지)uint8
- unsigned byte(부호 없는 바이트) (1바이트)int8
- signed byte(부호 있는 바이트) (1바이트)uint16
- unsigned ‘shorts’(부호 없는 ‘short’) (2바이트)int16
- signed ‘short’(부호 있는 ‘short’)(2바이트)uint32
- unsigned int(부호 없는 int) (4바이트)int32
- signed int(부호 있는 int) (4바이트)uint64
- unsigned ‘quad word’(부호 없는 ‘쿼드 단어’) (8바이트)int64
- signed ‘quad word’(부호 있는 ‘쿼드 단어’) (8바이트)float
- 단정밀도 부동 소수점(4바이트)double
- 배정밀도 부동 소수점(8바이트)PTRINT
- 포인터를 가질 수 있는 정수(PTRINT 크기 추정 금지)
C++의 int
및 부호 없는 int
타입(플랫폼에 따라 크기가 변할 수 있으나 항상 최소 너비는 32비트로 보장됨)은 정수 너비가 중요치 않은 경우라면 코드에서 사용해도 괜찮다. 명시적으로 크기가 정해진 타입은 여전히 시리얼라이즈 또는 리플리케이트된 포맷으로 사용해야 한다.
표준 라이브러리 사용
과거에는 UE에서 C와 C++ 표준 라이브러리를 직접 사용하는 것을 지양했다. 기에는 빠른 구현을 위한 자체 라이브러리 사용, 메모리 할당에 대한 제어력 강화, 널리 이용 가능해지기 전에 새 함수 기능 추가, 바람직하지만 비표준인 동작 변경 수행, 코드베이스 전체에서 문법 일관성 유지, UE 언어와 호환되지 않는 컨스트럭트 방지 등 여러 가지 이유가 있다. 하지만 최근 몇 년에 걸쳐 표준 라이브러리는 더욱 안정적이고 완성도가 높아져, 추상화 레이어로 래핑하거나 직접 재구현하지 않아도 되는 함수 기능을 포함하게 되었다.
UE 자체 라이브러리 대신 표준 라이브러리 기능을 사용할 것인지 선택할 때는 더 나은 결과를 제공하는 옵션을 선택하되, 일관성 또한 중요하게 고려해야 한다는 점을 명심해야 한다.
아래는 표준 라이브러리 사용에 대한 설명이다.
<atomic>
: 새 코드로 사용해야 하며 터치 시 기존 코드는 이주해야 합니다. Atomic은 지원되는 모든 플랫폼에서 완전히 효율적으로 구현되어야 합니다. 에픽의 자체 TAtomic
은 부분적으로만 구현되며 에픽에서 이를 유지보수하고 개선할 예정이 없습니다.
<type_traits>
: 레거시 UE 특성과 표준 특성 간에 겹치는 부분이 있는 경우 사용해야 합니다. 특성은 종종 정확도를 위해 컴파일러 고유 속성으로 구현되며, 컴파일러는 표준 특성을 파악하여 이를 일반 C++로 처리하는 대신 보다 빠른 컴파일 경로를 선택할 수 있습니다. 한 가지 우려되는 사항은 UE 특성이 보통 Value
static 또는 Type
typedef를 갖는 반면, 표준 특성은 value
및 type
을 사용하게 되어 있습니다. 이는 중요한 차이점으로, 컴포지션 특성에 의해 특정 문법(예: std::conjunction
)이 필요하기 때문입니다. 에픽에서 추가하는 새 특성은 컴포지션을 지원하기 위해 소문자 value
또는 type
으로 작성되고, 기존 특성은 대/소문자를 모두 지원하도록 업데이트됩니다.
<initializer_list>
: 중괄호로 묶인 이니셜라이저 문법을 지원하기 위해 사용되어야 합니다. 이는 언어와 표준 라이브러리가 겹치는 경우에 해당되며, 이를 지원해야 할 경우 대안은 없습니다.
<regex>
: 직접적으로 사용할 수도 있지만 에디터 전용 코드 내에 캡슐화해서 사용해야 합니다. 자체 정규 표현식 솔루션을 구현할 계획은 없습니다.
<limits>
: std::numeric_limits
를 온전히 사용할 수 있습니다.
<cmath>
: 이 헤더의 모든 부동 소수점 함수를 사용할 수 있습니다.
<cstring>
: memcpy()
및 memset()
는 명확한 퍼포먼스상의 이점이 있을 경우 각각 FMemory::Memcpy
및 FMemory::Memset
대신 사용할 수 있습니다.
표준 컨테이너와 스트링은 interop 코드를 제외하고는 사용하지 말아야 합니다.
Const 정확도
Const는 문서이자 컴파일러 지시어(directive)이므로, 모든 코드는 const 정확도를 맞추어야 한다.
여기에는 다음과 같은 경우가 포함된다.
- 함수 아규먼트가 함수에 의해 수정되지 않아 함수 아규먼트를 const 포인터 또는 참조로 전달하는 경우
- 메서드가 오브젝트를 수정하지 않아 const로 플래그를 지정하는 경우
- 루프에서 컨테이너 수정을 하지 않아 const를 사용하여 컨테이너에 반복작업을 하는 경우
예시:
void SomeMutatingOperation(FThing& OutResult, const TArray<Int32>& InArray)
{
// InArray는 여기서 수정되지 않지만, OutResult는 수정될 수도 있습니다.
}
void FThing::SomeNonMutatingOperation() const
{
// 이 코드는 자신을 호출한 FThing을 수정하지 않습니다.
}
TArray<FString> StringArray;
for (const FString& : StringArray)
{
// 이 루프의 바디는 StringArray를 수정하지 않습니다.
}
포인터가 가리키는 것이 아니라 포인터 자체를 const로 만들 때는 끝에 const 키워드를 넣습니다. 레퍼런스는 어떤 방식으로도 ‘재할당’ 불가하며, 같은 방법으로 const로 만들 수 없습니다.
예시:
// const 포인터에서 const 이외 오브젝트 - 포인터로의 재할당은 불가하나, T는 여전히 수정 가능합니다.
T* const Ptr = ...;
// 틀림
T& const Ref = ...;
복잡한 타입에 대한 이동 시맨틱이 제한되며 내장된 타입에는 컴파일 경고가 발생하므로 반환 타입에는 const를 사용하지 않습니다. 이 규칙은 반환 타입 자체에만 적용되며, 포인터의 타깃 타입 또는 반환되는 레퍼런스에는 적용되지 않습니다.
예시:
// 나쁜 예 - const 배열 반환
const TArray<FString> GetSomeArray(); // 복사 일어남
// 좋은 예 - const 배열로의 레퍼런스 반환
const TArray<FString>& GetSomeArray(); // 복사 안 일어나면서 수정 불가
// 좋은 예- const 배열로의 포인터 반환
const TArray<FString>* GetSomeArray(); // 포인터에 증감 연산 가능 + 내부 배열 수정 불가
// 나쁜 예 - const 배열로의 const 포인터 반환
const TArray<FString>* const GetSomeArray();
간단한 int
float
등에는 const
를 사용하는 것이 문제가 되지 않는다. 그러나 복잡한 타입은 const
를 사용하면 컴파일러가 경고를 발생 시킬 수 있다. 복사
나 이동
시멘틱에 영향을 줄 수 있기 때문이다.
강-타입 Enum
Enum 클래스는 항상 일반 열거형이든 UENUM
이든 기존 네임스페이스 열거형을 대체하여 사용해야 합니다. 예시는 다음과 같습니다.
// 기존 열거형
UENUM()
namespace EThing
{
enum Type
{
Thing1,
Thing2
};
}
// 새 열거형
UENUM()
enum class EThing : uint8
{
Thing1,
Thing2
}
이는 UPROPERTY
로도 지원되며, 기존 TEnumAsByte<>
우회법을 대체합니다. 열거형 프로퍼티는 바이트뿐만 아니라 어떤 크기라도 될 수 있습니다.
// 기존 프로퍼티
UPROPERTY()
TEnumAsByte<EThing::Type> MyProperty;
// 새 프로퍼티
UPROPERTY()
EThing MyProperty;
그러나 블루프린트에 노출되는 열거형은 여전히 uint8
기반이어야 합니다.
플래그로 사용되는 Enum 클래스는 새로운 ENUM_CLASS_FLAGS(EnumType)
매크로를 사용하여 비트 단위 연산자 전체를 다음과 같이 자동 정의할 수 있습니다.
enum class EFlags
{
None = 0x00,
Flag1 = 0x01,
Flag2 = 0x02,
Flag3 = 0x04
};
ENUM_CLASS_FLAGS(EFlags)
한 가지 예외는 truth 컨텍스트에서 플래그를 사용하는 것으로, 이는 언어상의 한계입니다. 대신 모든 플래그 열거형에 비교용으로 0으로 설정된 None
열거형을 넣습니다.
// 기존 스타일
if (Flags & EFlags::Flag1)
// 새 스타일
if ((Flags & EFlags::Flag1) != EFlags::None)
일반적인 스타일 문제
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();
}
함수 호출에서 익명 리터럴 사용은 피하세요. 이름 상수로 의미를 설명하는 것이 좋습니다.
예시:
// 기존 스타일
Trigger(TEXT("Soldier"), 5, true);.
// 새 스타일
const FName ObjectName = TEXT("Soldier");
const float CooldownInSeconds = 5;
const bool bVulnerableDuringCooldown = true;
Trigger(ObjectName, CooldownInSeconds, bVulnerableDuringCooldown);
이렇게 하면 함수 선언을 조회하지 않아도 이해할 수 있으므로 일반적인 독자가 의도를 쉽게 파악할 수 있습니다.
헤더에 특수한 스태틱 변수를 정의하지 않도록 합니다. 해당 헤더가 포함된 모든 이동 단위로 인스턴스가 컴파일되기 때문입니다.
// SomeModule.h
static const FString GUsefulNamedString = TEXT("String");
이러한 코드는 다음으로 대체해야 함
// SomeModule.h
extern SOMEMODULE_API const FString GUsefulNamedString;
// SomeModule.cpp
const FString GUsefulNamedString = TEXT("String");
Leave a comment