Choose your operating system:
Windows
macOS
Linux
에픽에서 저희는 단순한 코딩 표준과 규칙이 몇 가지 있습니다. 이 문서는 논의 중이거나 작업 중인 내용이라기보다는, 현재 에픽에서 시행 중인 코딩 표준 상태를 반영한 것입니다. 코딩 표준은 필수적으로 따라야 합니다.
프로그래머들에게 있어서, 코딩 규칙은 다음과 같은 여러 가지 이유에서 매우 중요합니다:
-
하나의 소프트웨어가 그 수명을 지속하는 동안 들어가는 경비 가운데 80%는 유지보수비입니다.
-
원저자가 그 소프트웨어의 수명이 다할 때까지 관리하는 일은 거의 없습니다.
-
코딩 규칙은 그 소프트웨어를 한층 읽기 쉽도록 해주므로, 엔지니어들은 새 코드를 보다 빨리 그리고 철저하게 이해할 수 있습니다.
-
만일 저희가 mod 커뮤니티 개발자들께 소스 코드를 공개하기로 결정한다면, 이해하기 쉬운 것이기를 바랍니다.
-
사실 이 규칙 가운데 상당수는 컴파일러 간의 호환을 위해 필요한 것이기도 합니다.
아래 코딩 표준은 C++ 에 특화되어 있지만, 표준이라는 개념은 어떤 언어를 사용하든 상관없이 따라야 하는 것입니다. 가급적 한 섹션에는 특정 언어에 대해 상응하는 규칙이나 예외가 제공될 수 있습니다.
클래스 체계
클래스 체계는 작성하는 사람 보다는 읽는 사람을 염두에 두고 체계를 잡아야 합니다. 읽는 사람 대부분은 클래스의 공용 인터페이스를 쓸 것이기에, public 을 먼저 선언하고, 그 후 클래스의 private 구현이 뒤따릅니다.
저작권 공지
에픽이 배포용으로 제공한 (.h, .cpp, .xaml 등의) 소스 파일은 반드시 파일의 첫 줄에 저작권 공지를 포함시켜야 합니다. 공지의 포맷은 반드시 다음과 같아야 합니다:
// Copyright Epic Games, Inc. All Rights Reserved.
이 줄이 없거나 포맷이 다르게 되어 있다면, CIS 가 오류를 내고 중단시킬 것입니다.
작명 규칙
-
모든 코드와 코멘트는 미국 영어 철자법과 문법을 사용해야 합니다.
-
(유형 이름이나 변수 이름 등) 이름 내 각 단어의 첫 글자는 대문자로 써야 하며, 단어 사이에 보통은 밑줄이 없습니다.
Health
와UPrimitiveComponent
정도를 예로 들 수는 있지만,lastMouseCoordinates
나delta_coordinates
같은 것은 아닙니다. -
변수 이름과 구분하기 위해 유형 이름을 대문자 한 글자로 나타내는 접두사를 붙입니다. 예를 들어
FSkin
이 유형 이름이고,Skin
은FSkin
의 인스턴스입니다.-
템플릿 클래스 접두사는 T입니다.
-
UObject
에서 상속하는 클래스 접두사는 U입니다. -
AActor
에서 상속하는 클래스 접두사는 A입니다. -
SWidget
에서 상속하는 클래스 접두사는 S입니다. -
추상 인터페이스인 클래스 접두사는 I입니다.
-
Enum(열거형)의 접두사는 E입니다.
-
Boolean(부울) 변수의 접두사는 b입니다(예:
bPendingDestruction
또는bHasFadedIn
). -
그 외 대부분 클래스의 접두사는 F이나, 일부 서브시스템은 다른 글자를 사용하기도 합니다.
-
Typedef 접두사는 적합한 유형을 붙여야 합니다: 구조체의 typedef인 경우 F,
UObject
등의 typedef인 경우 U입니다.-
특정 템플릿 인스턴스의 typedef는 더이상 템플릿이 아니라 그에 맞는 접두사를 붙여야 합니다, 예:
typedef TArray<FMytype> FArrayOfMyTypes;
-
-
C#에서는 접두사가 생략됩니다.
-
UnrealHeaderTool(언리얼 헤더 툴)은 대부분의 경우 올바른 접두사가 필수이므로, 제대로 붙여주는 것이 중요합니다.
-
-
유형과 변수명은 명사입니다.
-
메서드 이름은 동사로 그 메서드가 하는 일이나, 하는 일이 딱히 없는 경우 반환값을 설명합니다.
변수, 메서드, 클래스 이름은 명확하고 애매하지 않으며 서술적이여야 합니다. 이름의 범위가 클 수록, 서술적인 좋은 이름의 중요성 역시 커집니다. 과도한 축약은 피하시기 바랍니다.
모든 변수는 변수에 대한 설명을 코멘트로 붙일 수 있도록 한 번에 하나씩만 선언해야 합니다. 이는 JavaDocs 스타일에서도 요구하는 바입니다. 변수 앞에 여러 줄짜리든 한 줄짜리든 코멘트를 남기면 되며, 변수 그룹 목적으로 빈 줄을 띄워도 됩니다.
부울 값을 반환하는 모든 함수(예:
IsVisible()
또는
ShouldClearBuffer()
)는 true/false 질문을 합니다.
프로시져(반환값이 없는 함수)는 강한 동사 뒤에 오브젝트를 붙여 써야 합니다. 예외는 메서드의 오브젝트가 그 안에 있는 오브젝트일 때인데, 그런 경우 오브젝트는 컨텍스트에서 이해를 합니다. 'Handle' 이나 'Process' 같은 것으로 시작하는 이름은 애매하니 피해 주시기 바랍니다.
필수는 아니지만, 함수 파라미터 중 레퍼런스로 전달된 이후 함수가 그 값에 출력할 것으로 기대되는 것의 경우 이름 앞에 'Out' 접두사를 붙일 것을 추천합니다. 그래야 이 인수로 전달되는 값은 함수로 대체될 것임이 명확해집니다.
In 또는 Out 파라미터 역시 부울인 경우, In/Out 접두사 앞에 'b'를 붙입니다, 예:
bOutResult
.
값을 반환하는 함수는 반환값에 대한 설명을 해야 합니다. 이름을 통해 함수가 반환하게 될 값을 명확히 알 수 있어야 합니다. 이는 부울 함수의 경우 특히나 중요합니다. 다음 두 예시 메서드를 살펴 봅시다:
// true면 뭐라는 걸까요?
bool CheckTea(FTea Tea);
// 이름을 통해 true면 차가 신선하다는 것을 명확히 알 수 있습니다.
bool IsTeaFresh(FTea Tea);
예시
float TeaWeight;
int32 TeaCount;
bool bDoesTeaStink;
FName TeaName;
FString TeaFriendlyName;
UClass* TeaClass;
USoundCue* TeaSound;
UTexture* TeaTexture;
포팅 가능한 C++ 코드
-
bool
- boolean 값(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 크기 추정 금지)
C++ 의
int
와 unsigned
int
유형은 플랫폼에 따라 크기가 변할 수 있지만 너비가 적어도 32비트임이 보장되므로 정수 너비가 중요치 않은 경우라면 코드에서 사용해도 괜찮습니다. 명시적으로 크기가 정해진 유형은 여전히 시리얼라이즈 또는 리플리케이트된 포맷으로 사용해야 합니다.
표준 라이브러리 사용
과거에는 UE에서 C 및 C++ 표준 라이브러리를 직접 사용하는 것을 지양했습니다. 여기에는 빠른 구현을 위한 자체 라이브러리 사용, 메모리 할당에 대한 제어력 강화, 널리 이용 가능해지기 전에 새 함수 기능 추가, 바람직하지만 비표준인 동작 변경 수행, 코드베이스 전반에 문법 일관성 유지, UE 언어와 호환되지 않는 구성체 방지 등 여러 가지 이유가 있습니다. 하지만 최근 몇 년에 걸쳐 표준 라이브러리는 더욱 안정적이고 완성도가 높아져, 직접 재구현하거나 추상 레이어로 래핑하지 않아도 되는 함수 기능을 포함하고 있습니다.
이전에 사용한 적이 없는 새로운 표준 라이브러리 컴포넌트를 사용하고 싶다면 코딩 표준 그룹을 통해 평가받아야 합니다. 이를 통해 그러한 컴포넌트가 허용되면 이 화이트리스트 컴포넌트 목록을 최신 상태로 유지할 수 있습니다.
자체 라이브러리 대신 표준 라이브러리 기능을 사용할 것인가에 대해 선택해야 할 때는 더 나은 결과를 제공하는 옵션을 선택하되, 일관성 또한 중요하게 고려해야 한다는 점을 명심하세요. 레거시 UE 구현이 더 이상 도움이 되지 않을 경우, 폐기하고 모든 사용을 표준 라이브러리로 이주하기로 선택할 수도 있습니다.
동일한 API에서 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>
:
여기
에 나온 대로 이 헤더의 부동 소수점 비교 함수만 사용할 수 있습니다.
표준 컨테이너와 스트링은 interop code를 제외하고는 사용하지 말아야 합니다.
코멘트
코멘트는 소통이고, 소통은 중요합니다. 코멘트에 대해 명심하실 점이 몇 가지 있습니다(출처: Kernighan & Pike의 The Practice of Programming ).
지침
-
자체적으로 설명이 되는 코드를 작성하세요:
// 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 포인터 또는 레퍼런스 전달하는 경우
-
메서드가 오브젝트를 수정하지 않아 const 플래그를 붙이는 경우
-
루프에서 컨테이너 자체에 대한 수정을 하지 않아 const를 사용하여 컨테이너에 반복작업을 하는 경우입니다.
예:
void SomeMutatingOperation(FThing& OutResult, const TArray<Int32>& InArray)
{
// InArray는 SomeMutatingOperation에 의해 수정되지 않지만, OutResult는 수정될 수도 있습니다.
}
void FThing::SomeNonMutatingOperation() const
{
// 이 코드는 자신을 부른 FThing을 수정하지 않습니다.
}
TArray<FString> StringArray;
for (const FString& : StringArray)
{
// 이 루프의 바디는 StringArray를 수정하지 않습니다.
}
const는 by-value 함수 파라미터와 로컬에 쓰기에도 좋습니다. 그러면 변수가 함수 바디에서 수정되지 않을 것이라고 알려주므로 가독성 향상에 도움이 됩니다. 이렇게 하면 선언과 정의부가 일치되는데, JavaDoc 프로세스에 영향을 줄 수 있습니다.
예:
void AddSomeThings(const int32 Count);
void AddSomeThings(const int32 Count)
{
const int32 CountPlusOne = Count + 1;
// Count도 CountPlusOne도 함수 바디에서 변경 불가능합니다.
}
여기에 대한 한 가지 예외는 값 전달 파라미터인데, 이 파라미터는 궁극적으로 컨테이너 속으로 이동됩니다만('이동 시맨틱' 참고), 이는 드문 경우입니다.
예:
void FBlah::SetMemberArray(TArray<FString> InNewArray)
{
MemberArray = MoveTemp(InNewArray);
}
포인터를 (포인터가 가리키는 것이 아니라) 자체를 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();
예시 포맷
저희는 JavaDoc 기반 시스템을 사용하여 코드에서 코멘트를 자동으로 추출한 뒤 문서를 만들기 때문에, 코멘트에는 따라야 하는 특수한 포맷 규칙이 몇 가지 있습니다.
다음 예시는 클래스, 스테이트, 메서드, 변수 코멘트의 포맷을 선보입니다. 기억하실 것은, 코멘트는 코드를 증강시켜야 한다는 것입니다. 코드는 구현을 설명하고, 코멘트는 그 의도를 설명합니다. 코드 한 줄의 의도를 바꾸더라도 반드시 코멘트를 업데이트하시기 바랍니다.
참고로 지원되는 파라미터 코멘트 스타일은 두 가지로,
Steep
와
Sweeten
메서드로 표시됩니다.
Steep
이 사용하는
@param
스타일은 전형적인 여러 줄 스타일이지만, 단순 함수의 경우 파라미터와 반환 값 문서를 함수에 대한 설명 코멘트로 통합시키는 것이, Sweeten 예시에서 보듯이 더욱 깔끔할 수 있습니다.
@see
또는
@return
같은 특수 코멘트 태그는 기본 설명에 이어 새 줄의 시작에만 사용해야 합니다.
메서드 코멘트는 딱 한 번, 메서드가 공개적으로 선언되는 곳에 include시켜야 합니다. 메서드 코멘트는 호출자에게 관련이 있을 메서드 오버라이드 관련 정보를 포함해서, 메서드 호출자에 관련된 정보만을 담아야 합니다. 메서드 구현에 대한 세부사항이나 호출자에 관련이 없는 오버라이드는 메서드 구현 안에 코멘트를 달아야 할 것입니다.
/** 마실 수 있는 오브젝트에 대한 인터페이스입니다. */
class IDrinkable
{
public:
/**
* 플레이어가 이 오브젝트를 마실 때 호출됩니다.
* @param OutFocusMultiplier - 반환되면 마신 사람의 포커스에 적용할 배수가 들어갑니다.
* @param OutThirstQuenchingFraction - 반환되면 마신 사람의 갈증 해소 정도가 들어갑니다 (0-1).
* @warning Drink 준비가 제대로 된 이후에만 호출하세요.
*/
virtual void Drink(float& OutFocusMultiplier, float& OutThirstQuenchingFraction) = 0;
};
/** 차 한 잔입니다. */
class FTea : public IDrinkable
{
public:
/**
* 우려내는 데 사용한 물의 용량과 온도가 주어진 경우 차에 대한 델타-테이스트 값을 계산합니다.
* @param VolumeOfWater - 우려내는 데 사용한 물의 양(mL) 입니다.
* @param TemperatureOfWater - 물의 온도(켈빈)입니다.
* @param OutNewPotency - 담그기 시작한 이후의 차의 효능으로, 0.97에서 1.04까지입니다.
* @return 차 강도의 변화를 분당 차 맛 단위(TTU) 로 반환합니다.
*/
float Steep(
const float VolumeOfWater,
const float TemperatureOfWater,
float& OutNewPotency
);
/** 차에 감미료를 추가합니다. 같은 당도를 내는 데 필요한 자당의 그램으로 측정합니다. */
void Sweeten(const float EquivalentGramsOfSucrose);
/** 일본에서 판매되는 차의 엔화 단위 가치입니다. */
float GetPrice() const
{
return Price;
}
virtual void Drink(float& OutFocusMultiplier, float& OutThirstQuenchingFraction) override;
private:
/** 엔화 단위 가격입니다. */
float Price;
/** 현재 당도로, 자당 그램 단위입니다. */
float Sweetness;
};
float FTea::Steep(const float VolumeOfWater, const float TemperatureOfWater, float& OutNewPotency)
{
...
}
void FTea::Sweeten(const float EquivalentGramsOfSucrose)
{
...
}
void FTea::Drink(float& OutFocusMultiplier, float& OutThirstQuenchingFraction)
{
...
}
클래스 코멘트에 포함되는 것은?
-
이 클래스가 해결하는 문제에 대한 설명과,
-
이 클래스를 생성한 이유입니다.
그런 모든 여러 줄 메서드 코멘트 부분이 뜻하는 바는?
-
함수의 목적:
이 함수가 해결하는 문제
를 설명합니다. 위에서 말씀드린 것처럼, 코멘트는의도
를 설명하며, 코드는구현
을 설명합니다. -
파라미터 코멘트: 각 파라미터 코멘트에 포함되는 것은:
-
측정 단위,
-
예상되는 값 범위,
-
'불가능한' 값,
-
상태/오류 코드의 의미입니다.
-
-
반환 코멘트: 여기에는 예상되는 반환 값을, 단지 출력 변수로 문서화합니다. 중복을 피하기 위해, 함수의 목적이 오로지 이 값을 반환하는 것이고 그 부분이 함수 목적에 문서화된 경우 명시적 @return 코멘트는 사용하지 말아야 할 것입니다.
-
추가 정보: 선택적으로
@warning
,@note
,@see
,@deprecated
정도를 사용해서 관련이 있는 추가 정보를 문서화할 수 있습니다. 각각은 나머지 코멘트에 이어 별도의 줄에 선언해야 합니다.
최신 C++ 언어 문법
언리얼 엔진은 다수의 C++ 컴파일러로 대규모 포팅이 가능하도록 만들어졌기에, 기능을 사용할 때는 지원하게 될 수도 있다고 생각되는 컴파일러와의 호환성을 신중히 따져 봅니다. 가끔은 매우 유용한 기능이라 매크로에 저장하여 많이 사용하는 경우도 있습니다. 그러나 보통은 지원하게 될 거라 생각하는 모든 컴파일러가 최신의 표준을 지원할 때까지는 기다립니다.
범위 기반 for, 이동 시맨틱, 람다와 캡처 이니셜라이저처럼 최신 컴파일러에서 잘 지원되는 것으로 보이는 많은 C++14 언어 기능을 활용하고 있습니다. 어떤 경우에는 (컨테이너의 rvalue 레퍼런스 같은) 전처리기 조건문에서 이러한 기능을 묶어 사용할 수 있도록 하고 있습니다. 그러나 새 플랫폼에서 문법을 소화시키지 못하여 혼란이 야기될 수 있는 기능에 대해서는, 확신이 들기 전까지 채택하지 않을 수 있습니다.
아래에 지원되는 최신 C++ 컴파일러 기능이라 명시한 것 이외의 컴파일러 전용 언어 기능에 대해서는, 전처리기 매크로나 조건문에 묶어두지 않고서는 사용을 삼가야 하며, 그랬다 해도 조심히 사용해야 합니다.
static_assert
이 키워드는 컴파일 시간 어서트가 필요한 경우에 사용할 수 있습니다.
override 및 final
이 키워드들은 사용할 수 있을 뿐 아니라, 사용을 강력히 권합니다. 이들이 빠진 곳이 많이 있을 수 있으나, 서서히 고쳐갈 것입니다.
nullptr
모든 경우 C 스타일
NULL
매크로 대신
nullptr
을 사용해야 합니다.
이에 대한 한 가지 예외라면, C++/CX 빌드(예: Xbox One)의
nullptr
은 사실 managed null 레퍼런스 유형입니다. 유형이나 어떤 템플릿 인스턴스화 컨텍스트를 제외하고는 네이티브 C++ 의 nullptr 과 거의 호환되므로, 호환성을 위해서는 좀 더 일반적인
decltype(nullptr)
대신
TYPE_OF_NULLPTR
매크로를 사용해야 합니다.
'auto' 키워드
아래 몇 가지 예외를 제외하고 C++ 에서
auto
를 사용해서는 안됩니다. 항상 초기화시키려는 유형은 명시해 줘야 합니다. 그 유형이 독자에게 명확히 보여야 한다는 뜻입니다. 이 규칙은 C#의
var
키워드 사용에도 적용됩니다.
auto
를 사용해도 괜찮은 경우는?
-
변수에 람다를 바인딩해야 하는 경우. 람다 유형은 코드로 표현 가능하지 않기 때문입니다.
-
이터레이터 변수의 경우. 단, 이터레이터 유형이 매우 장황하여 가독성에 악영향을 미치는 경우에 한합니다.
-
템플릿 코드에서, 표현식의 유형을 쉽게 식별할 수 없는 경우. 이는 고급에 해당하는 경우입니다.
코드를 읽는 사람이 유형을 명확하게 알 수 있어야 한다는 것은 매우 중요합니다. 일부 IDE 에서 유형을 추론할 수는 있지만, 이는 코드가 안정적인 상태라는 가정하에서입니다. GitHub 같은 곳에서 개별 소스 파일을 독립적으로 확인하거나, merge/diff 툴을 사용하는 사람에게도 도움이 되지 않습니다.
auto
를 사용해도 괜찮다고 확실히 알고 있는 경우, 항상 해당 유형에 const, &, *를 정확히 사용해야 한다는 점 기억해 주시기 바랍니다. 그렇게 해야 `auto` 를 통해 추론 유형을 원하는 유형으로 이끌어낼 수 있을 것입니다.
범위 기반 for
코드의 가독성과 유지보수성 향상에 도움이 되므로 사용을 추천합니다. 이전
TMap
이터레이터를 사용하는 코드를 이주할 때는, 이전 이터레이터 유형 메서드였던
Key()
와
Value()
함수가 이제 단순히 내재된 키-값
TPair
의
Key
및
Value
필드가 되었음에 유의하세요.
예:
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());
}
람다 및 무명 함수
람다(lambda)는 자유롭게 사용할 수 있습니다. 최적의 사용법은 길이상 두 구문 정도가 되어야 합니다. 특히나 규모가 더 큰 표현식이나 구문의 일부로 사용될 때, 예를 들면 범용 알고리즘의 술부(predicates)에 사용될 때는 더욱 그러해야 합니다.
예:
// 이름에 'Hello' 단어가 포함된 첫 번째 Thing을 검색합니다.
Thing* HelloThing = ArrayOfThings.FindByPredicate([](const Thing& Th){ return Th.GetName().Contains(TEXT("Hello")); });
// 배열을 이름 역순으로 정렬합니다.
Algo::Sort(ArrayOfThings, [](const Thing& Lhs, const Thing& Rhs){ return Lhs.GetName() > Rhs.GetName(); });
주의할 점이라면, 스테이트풀 람다는 많이 사용하는 경향이 있는 함수 포인터에 할당할 수 없습니다.
사소하지 않은 람다의 문서화는 일반 함수와 같은 방식으로 문서화해야 합니다. 코멘트를 몇 줄에 걸쳐 나눠 붙여도 됩니다.
자동 캡처보다 수동 캡처가 좋습니다(
[&]
및
[=]
). 커다란 람다와 유예식(deferred) 실행에 사용되는 경우 가독성, 유지보수성, 퍼포먼스 측면에서 특히 중요합니다. 저자의 의도를 선언하므로 코드 리뷰 과정에서 실수를 더욱 쉽게 잡아낼 수 있습니다. 잘못된 캡처는 안 좋은 결과가 생길 수 있으며, 코드 유지보수 과정에서 문제가 될 확률이 높습니다.
-
(
this
포인터 포함) 포인터 참조 캡처와 값 캡처가 때때로 허상 참조를 유발할 수 있는데, 람다 실행이 유예된 경우가 그렇습니다. -
값 캡처는 유예식이 아닌 람다에 불필요한 사본을 만드는 경우 퍼포먼스 우려가 있을 수 있습니다.
-
잘못 캡처된 UObject 포인터는 가비지 콜렉터에 보이지 않습니다. 자동 캡처는 멤버 변수가 참조된 경우, 묵시적으로
this
를 캡처합니다. 물론[=]
가 람다에 모든 것의 별도 사본이 있다는 인상을 주기는 하지만 말입니다.
커다란 람다이거나 다른 함수 호출의 결과를 반환할 때는 명시적 반환형을 사용해야 합니다.
auto
키워드와 같은 방식으로 고려해야 합니다:
// 여기서는 반환형이 없어 반환형이 명확하지 않습니다.
auto Lambda = []() -> FMyType
{
return SomeFunc();
}
Sort 호출처럼 시맨틱이 명확해서 명시해 줘도 과잉 친절일 뿐인 사소한 비유예식 람다에는 자동 캡처와 묵시적 반환형을 사용해도 됩니다.
C++14의 캡처 이니셜라이저 기능을 사용해도 됩니다:
TUniquePtr<FThing> ThingPtr = MakeUnique<FThing>();
AsyncTask([UniquePtr = MoveTemp(UniquePtr)]()
{
// 여기에 UniquePtr 사용
});
강 유형 Enum
Enum 클래스는 항상 일반 enum 이든
UENUM
이든 구식 네임스페이스 enum을 대체하여 사용해야 합니다. 예:
// 옛 enum
UENUM()
namespace EThing
{
enum Type
{
Thing1,
Thing2
};
}
// 새 enum
UENUM()
enum class EThing : uint8
{
Thing1,
Thing2
}
이는
UPROPERTY
로도 지원되며, 옛
TEnumAsByte<>
우회법을 대체합니다. enum 프로퍼티는 바이트뿐만 아니라 어떤 크기도 될 수 있습니다:
// 옛 프로퍼티
UPROPERTY()
TEnumAsByte<EThing::Type> MyProperty;
// 새 프로퍼티
UPROPERTY()
EThing MyProperty;
그러나 블루프린트에 노출되는 enum은 여전히
uint8
기반이어야 합니다.
플래그로 사용되는 Enum 클래스는 새로운
ENUM_CLASS_FLAGS(EnumType)
매크로를 사용하여 비트단위 연산자 전부를 자동 정의할 수 있습니다:
enum class EFlags
{
None = 0x00,
Flag1 = 0x01,
Flag2 = 0x02,
Flag3 = 0x04
};
ENUM_CLASS_FLAGS(EFlags)
여기에 한 가지 예외라면,
truth
컨텍스트에서 플래그를 사용하는 것인데, 이는 언어상의 한계입니다. 그 대신, 모든 플래그 enum에 비교용으로 0 설정된
None
enum을 넣도록 합니다:
// 옛 스타일
if (Flags & EFlags::Flag1)
// 새 스타일
if ((Flags & EFlags::Flag1) != EFlags::None)
이동 시맨틱
모든 주요 컨테이너 유형,
TArray
,
TMap
,
TSet
,
FString
에는 move 생성자와 move 할당 연산자가 있습니다. 이러한 유형을 값으로 전달/반환할 때 종종 자동으로 사용되지만,
std::move
의 UE 해당 버전인
MoveTemp
를 통해 명시적으로 호출 가능합니다.
값으로 컨테이너나 스트링을 반환하는 것은, 보통 임시로 복사하는 비용이 없어 표현성에 이득이 될 수 있습니다. 값 전달 관련 규칙 및
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 파일의 생성자에 두게 되면 가독성과 유지보수성에 좋지 않을 수 있습니다.
실제 사용할지 여부는 적절한 판단에 맡길 부분입니다. 경험적으로, 디폴트 멤버 이니셜라이저는 엔진 코드보다 게임 코드 쪽에 적합합니다. 기본값에 환경설정 파일을 사용하는 것도 고려해 보세요.
서드 파티 코드
엔진에서 사용하는 라이브러리에 코드를 수정할 때마다, 변경 내용에 //@UE4 코멘트는 물론 왜 변경했는지에 대한 설명이 되는 태그를 꼭 달아주세요. 그래야 그 라이브러리의 새 버전으로 변경 내용을 병합하는 작업이 쉽게 이루어지며, 라이선시 역시 우리가 가한 수정 내용을 쉽게 찾을 수 있습니다.
그리고 엔진에 포함되는 서드 파티 코드는 쉽게 검색되는 포맷의 코멘트로 마킹해야 합니다. 예:
// @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 블록에다 다른 줄을 추가하게 될 수가 있습니다. if 문의 영향을 받지 말아야 할 줄이라면 안 좋은 일이겠지요. 더욱 안 좋은 예라면 조건에 따라 컴파일되는 항목이 if/else 문을 깨지게 만드는 것입니다. 그러니 항상 중괄호로 묶어 주시기 바랍니다.
if (bHaveUnrealLicense)
{
InsertYourGameHere();
}
else
{
CallMarkRein();
}
여러 갈래 if 문에서 각각의 else if 문은 첫 번째 if 문과 같은 양만큼 들여써 줘야 합니다. 그래야 읽는 사람이 구조를 쉽게 이해할 수 있습니다:
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#로 코드를 작성한다면 스페이스가 아니라 탭을 사용해 주시기 바랍니다. C#와 C++ 사이의 전환은 프로그래머에게 자주 있는 일이고, 대부분은 일관된 탭 세팅을 주로 사용하기 때문입니다. Visual Studio 기본값으로는 C# 파일에 스페이스를 사용하고 있으니, 언리얼 엔진 코드 작업을 할 때는 이 세팅을 바꿔줘야 한다는 점 기억해 주시기 바랍니다.
Switch 문
빈 케이스(똑같은 코드를 갖는 다중 케이스)를 제외하고, switch case 문에서는 다음 케이스로 넘어가는지를 명시적으로 밝혀줘야 합니다. 각각의 경우마다 break를 넣던가, fall through 코멘트를 달아 주세요. 다른 코드 제어-이동 명령(return, continue 등)도 괜찮습니다.
default case는 항상 만들어 두시고, 다른 사람이 그 뒤에 새로운 케이스를 추가할 때에 대비해 break도 넣어 두시기 바랍니다.
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
선언을 사용해서 네임스페이스 안의 특정 변수만 자신의 범위로 에일리어싱할 수 있습니다(예: usingFoo:FBar
). 그러나 언리얼 코드에서는 보통 그렇게 하지 않습니다. -
매크로는 네임스페이스 내에 있을 수 없지만, 대신
UE_
접두사를 붙이면 됩니다(예:UE_LOG
).
물리적 종속성
-
파일 이름에는 가급적 접두사를 붙이지 않는 것이 좋습니다. 예를 들면
UScene.cpp
보다는Scene.cpp
가 좋습니다. 그래야 Workspace Whiz나 Visual Assist같은 툴에서 Open File in Solution 같은 기능을 사용할 때, 원하는 파일을 명확히 구분해 내는 데 필요한 글자 수를 줄이는 등 사용하기가 용이해집니다. -
모든 헤더는
#pragma once
디렉티브(지시자)로 복수의 include를 방지해야 합니다. 참고로 요즘의 컴파일러는 전부#pragma once
를 지원합니다.#pragma once //<파일 콘텐츠>
-
일반적으로는 물리적 결합을 최소화시켜 보세요. 특히, 다른 헤더의 표준 라이브러리 헤더를 include하지 마세요.
-
헤더 include 대신 앞선 선언이 가능하면, 그리 하세요.
-
include할 때는 가급적 세세하게 하세요. 예를 들어 Core.h를 include하지 마시고, Core의 헤더 중 정의가 필요한 특정 부분을 include하세요.
-
세세한 include 작업을 쉽게 하기 위해, 필요한 헤더는 전부 직접 include 해 주세요.
-
자신이 include한 다른 헤더에 의해 간접적으로 include되는 헤더에 의존하지 마세요.
-
다른 헤더를 통해 include시키기 보다는, 필요한 것을 전부 include 하세요.
-
모듈에는 Private과 Public 소스 디렉터리가 있습니다. 다른 모듈이 필요로 하는 정의는 Public 디렉터리의 헤더에 있어야 합니다. 그 외 모든 것은 Private 디렉터리에 있어야 할 것입니다. 참고로 구형 언리얼 모듈의 경우 이 디렉터리는 'Src' 및 'Inc'로 불리기도 하는데, 이름만 그럴 뿐 같은 방식으로 프라이빗 코드와 퍼블릭 코드를 구분하기 위함일 뿐이지, 헤더 파일을 소스 파일과 구분하기 위함은 아닙니다.
-
미리 컴파일된 헤더 생성용 헤더 설정에 대해서는 걱정하지 마세요. UnrealBuildTool이 더욱 잘 처리해 줄 것입니다.
-
큰 함수는 논리적 하위 함수로 나눕니다. 컴파일러 최적화 중 한 분야가 공통 하위 표현식 삭제입니다. 함수가 클 수록 그 식별을 위해 컴파일러가 할 일이 많아집니다. 그러면 빌드 시간이 크게 늘어나게 됩니다.
-
인라인 함수는 너무 많이 사용하지 마세요. 사용하지 않는 파일에 있어도 강제로 리빌드시키기 때문입니다. 인라인 함수는 사소한 접근자에만, 또는 프로파일링을 통해 이득이 있는 것으로 보일때만 사용해야 합니다.
-
FORCEINLINE
사용에 있어서는 조금 더 보수적이어야 합니다. 모든 코드와 로컬 변수는 호출하는 함수로 확장되어, 큰 함수에서 발생하는 것과 동일한 빌드 시간 문제가 생깁니다.
캡슐화
보호 키워드로 캡슐화시키세요. 클래스 멤버는 클래스의 public/protected 인터페이스 일부가 아닌 한, 거의 항상 private으로 선언해야 합니다. 상황에 따라 판단을 잘 하시되, 접근자가 없으면 나중에 기존 프로젝트와 플러그인 해체 없이 리팩터링 작업을 하는 것이 힘들어진다는 점 염두에 둬 주시기 바랍니다.
특정 칸은 파생 클래스에서만 사용하도록 의도된 경우, 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 할 수 있습니다.
-
이렇게는 되어도:
FShaderType* Ptr
이렇게는 안됩니다:
FShaderType *Ptr FShaderType * Ptr
-
변수 음영(shadowed)은 허용되지 않습니다. C++ 에서는 외부 영역에서의 변수를 음영 처리하는 것이 가능한데, 독자에게 모호할 수 있습니다. 예를 들어, 이 멤버 함수에서
Count
변수를 쓸 수 있는 방식이 세 가지입니다: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);
이러한 형태는 플래그가 실수로 반전되는 것을 막아주어, 포인터와 정수 인수에서 실수로 변환되는 현상이 방지되고, 중복 기본값을 반복할 필요도 없어서 더욱 효율적입니다.
bools
를 인수로 사용해도 괜찮은 경우는 setter처럼 함수에 전달하기에 완전한 상태일 때로, 예를 들면void FWidget::SetEnabled(bool bEnabled)
입니다. 변한다면 리팩터링을 고려하기는 해야 합니다. -
너무 긴 함수 파라미터 리스트는 피하세요. 함수가 파라미터를 많이 받는 경우 전용 구조체 전달을 고려해 보세요:
// 옛 스타일 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; }
override
키워드는 최근에 추가되어서 아직 이 규칙을 따르지 않는 기존 코드가 많습니다. 그런 코드에는 가급적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
이유: 엔진은 플랫폼별 세부 정보를 중앙에서 관리하므로, 그런 세부 정보를 플랫폼별 소스 파일 안에만 있도록 할 수 있습니다. 그렇게 하면 여러 플랫폼에 걸쳐 엔진을 유지하기가 쉬워지며, 플랫폼별 디파인에 대한 코드베이스를 수색할 필요 없이 새 플랫폼에 코드를 포팅할 수도 있습니다.
플랫폼 코드를 플랫폼별 폴더에 유지하는 것은 PS4, XboxOne, Nintendo Switch와 같은 NDA 플랫폼에 필수이기도 합니다.
[PLATFORM]
하위 디렉터리 존재 여부와 상관없이 코드가 컴파일되고 실행되도록 하는 것이 중요합니다. 즉, 크로스 플랫폼 코드가 플랫폼별 코드에 종속되어서는 안됩니다.