Unreal Engine 5 - UE C++ 코딩 표준) 복습을 위해 작성하는 글 2023-11-23

rizz·2023년 11월 23일
0

Unreal Engine 5

목록 보기
1/1

📒 갈무리 - UE C++ 코딩 표준

📌 UE C++ 코딩 표준

  • 통일된 방식으로 코드를 작성해야 코드를 분석하는 데 있어서 소비되는 시간을 줄일 수 있다.

📌 클래스 체계

  • 클래스는 작성자보다는 읽는 사람들 염두에 두어 작성되어야 하므로 퍼블릭 인터페이스를 먼저 선언한 후 구현한다.

📌 명명 규칙

  • 언리얼 엔진은 파스칼 케이싱만을 사용한다.
  • 템플릿 클래스는 접두사 T로 시작한다.
  • UObject에서 상속받는 클래스는 접두사 U로 시작한다.
  • AActor에서 상속받는 클래스는 접두사 A로 시작한다.
  • SWidget에서 상속받는 클래스는 접두사 S로 시작한다.
  • 추상 인터페이스 클래스는 접두사 I로 시작한다.
  • 열거형은 접두사 E로 시작한다.
  • bool 변수는 접두사 b로 시작한다.
  • 그 외의 클래스는 접두사 F로 시작한다. (언리얼로부터 상속받지 않는 클래스와 구조체)
  • bool 형을 반환하는 함수는 질의형으로 이름을 작성한다.
  • 함수 파라미터가 참조로 전달된 후 함수가 그 값을 쓸 것으로 예상되는 경우에는 이름 앞에 접두사 Out을 추가할 것을 권장한다.

📌 포용적 단어 선택

  • 편견을 보이는 은유나 직유는 사용하지 않는다. (blacklist, whitelist 등)
  • 역사상의 트라우마나 실제 차별받았던 경험을 연상시키는 단어를 사용하지 않는다. (slave, master, nuke)
  • 가상의 인물을 지정할 때는 단수형이더라도 대명사 they, them, their을 사용한다.
  • 사람 이외의 사물을 지정할 때는 it 및 its를 사용한다. (모듈, 플러그인 함수, 클라이언트, 서버, 기타 컴포넌트 등)
  • 성별이 없는 요소에 성별을 부여하지 않는다.
  • 성별을 상징하게 하는 guys와 같은 집합 명사를 사용하지 않는다.
  • 'a poor man's X'와 같이 임의의 성별이 포함된 구어체 문구 사용을 지양한다.

📌 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 크기 추정 금지)

  • int가 아닌 int숫자를 사용한다.
  • 문자열 또한 char이 아닌 TCHAR을 사용한다.
  • C++의 int 및 부호 없는 int 타입은 정수 너비가 중요하지 않은 경우라면 괜찮지만(최소 32비트로 보장됨), 플랫폼에 따라 크기가 변할 수 있으니 시리얼라이즈 또는 리플리케이트된 포맷을 사용해야 한다.
  • C++ 언어나 게임 엔진을 사용하는 것들은 메모리를 최적으로 관리하여 하나의 컴퓨터에서 최적의 효과를 내는 데에 집중이 되어 있다. 그렇기 때문에 자료형을 쓸 때 자료형의 크기를 파악하는 것 또한 중요하기 때문이다.

📌 표준 라이브러리

  • 표준 라이브러리는 범용적으로 설계가 되어있기 때문에 표준 라이브러리를 게임 엔진에 사용하는 순간부터는 컴파일 시간 및 복잡도가 증가하기 때문에 사용하지 않는다.
  • 동일한 API에서 UE 언어와 표준 라이브러리 언어를 혼합하여 사용하지 않는다. (memcpy(), memset() 등)

📌 코멘트

  • 자체적으로 설명이 되는 코드를 작성하라
// Bad:
t = s + l - b;

// Good:
TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves;
- 도움이 되는 코멘트를 작성하라
```cpp
// Bad:
// Leaves 증가
++Leaves;

// Good:
// 찻잎이 더 있다는 것을 알았습니다.
++Leaves;
  • 나쁜 코드에 코멘트를 달지 말라 - 그냥 다시 작성하라:
// Bad:
// 잎의 총개수는
// 작은 잎과 큰 잎을 더한 것에서
// 둘 다인 것을 뺀 것입니다.
t = s + l - b;

// Good:
TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves;
  • 코드를 모순되게 만들지 말라:
// Bad:
// Leaves 절대 증가 아님!
++Leaves;

// Good:
// 찻잎이 더 있다는 것을 알았습니다.
++Leaves;

 

📌 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가 아니기 때문에 가리키는 데이터 자제는 변경 가능.
T* const Ptr = ...;

// 레퍼런스 자체에 const라는 의미가 내포되어 있다.
T& const Ref = ...;
// 나쁨 - const 배열 반환
// 복사가 일어난다.
const TArray<FString> GetSomeArray();

// 좋음 - const 배열로의 레퍼런스 반환
// 복사가 일어나지 않는다.
// TArray에 있는 데이터를 고칠 수 없다.
const TArray<FString>& GetSomeArray();

// 좋음 - const 배열로의 포인터 반환
// 증감연산 가능
// 포인터가 가리키는 데이터는 수정 불가
const TArray<FString>* GetSomeArray();

// 나쁨 - const 배열로의 const 포인터 반환
// 포인터 수정 불가
// 포인터가 가리키는 데이터 수정 불가
const TArray<FString>* const GetSomeArray();

 

📌 C++ 언어 문법(C++17)

  • override 및 final 사용을 강력히 권장한다.
  • NULL보다는 nullptr을 사용해야 한다.
  • 변수에 lambda를 바인딩해야 하는 경우, iterator 변수가 가독성을 해치는 경우를 제외하고 일반적으로는 'auto' 키워드를 사용하지 않는다.
  • 옛 스타일의 for보다 새 스타일의 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);
}
  • lambda는 명시적으로 사용한다.
  • 옛 enum보다는 새 enum을 사용한다.
// 옛 enum
UENUM()
namespace EThing
{
    enum Type
    {
        Thing1,
        Thing2
    };
}

// 새 enum
UENUM()
enum class EThing : uint8
{
    Thing1,
    Thing2
}
  • std::move 대신 MoveTemp를 사용한다.
  • 멤버를 초기화할 때 헤더에 초기화하기보다는 생성자 구문에서 초기화한다.
  • 단일 구문에도 중괄호를 작성한다.
if (bThing)
{
    return;
}
  • 들여쓰기는 tab을 사용한다.
  • 탭 크기는 4자로 설정한다.
  • switch case 문에서는 break, return, continue, fall through 코멘트 등을 활용하여
    다음 케이스로 넘어가는지를 명시적으로 밝혀준다.
  • default case는 항상 작성해 준다.
switch (condition)
{
    case 1:
        ...
        // falls through

    case 2:
        ...
        break;

    case 3:
        ...
        return;

    case 4:
    case 5:
        ...
        break;

    default:
        break;
}
  • 파일 이름에는 접두사를 붙이지 않는다.
  • 헤더에서는 #pragema once를 반드시 작성한다.
  • include는 가능한 한 세밀하게 지정해야 한다. (가급적 적은 수를 include 하라)
  • 클래스 멤버는 거의 항상 private로 선언하고 getter와 setter로 접근한다.
  • 더 이상 파생시킬 클래스가 아닌 경우 final을 사용한다.
  • 클래스는 다른 클래스에 영향을 주지 않게 설계해야 한다.
  • 문자열을 표현할 때는 TEXT() 매크로를 사용한다.
  • 코드가 길어지더라도 가독성이 좋게 수정하는 것이 좋다. (이름도 마찬가지)
// Bad:
if ((Blah->BlahP->WindowExists->Etc && Stuff) &&
    !(bPlayerExists && bGameStarted && bPlayerStillHasPawn &&
    IsTuesday())))
{
    DoSomething();
}
// Good:
const bool bIsLegalWindow = Blah->BlahP->WindowExists->Etc && Stuff;
const bool bIsPlayerDead = bPlayerExists && bGameStarted && bPlayerStillHasPawn && IsTuesday();
if (bIsLegalWindow && !bIsPlayerDead)
{
    DoSomething();
}
  • 포인터와 레퍼런스의 스페이스는 오른쪽에 딱 한 칸만 둬야 한다.
    그래야 특정 유형에 대한 모든 포인터나 레퍼런스를 빠르게 Find in Files 할 수 있다.
// Good:
FShaderType* Ptr

// Bad:
FShaderType *Ptr
FShaderType * Ptr
  • 헤더에 static 변수를 선언하게 되면 그 헤더를 참조하는 모둔 인스턴스에
    컴파일되기 때문에 cpp에서 구현한다.
// Bad:
// SomeModule.h
static const FString GUsefulNamedString = TEXT("String");

// Good:
// SomeModule.h
extern SOMEMODULE_API const FString GUsefulNamedString;

// SomeModule.cpp
const FString GUsefulNamedString = TEXT("String");
  • bool 함수 파라미터는 사용을 피하고 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);
  • 함수 파라미터를 길게 사용하는 것을 피하고 구조체를 사용하여 전달한다.
  • 인터페이스 클래스는 멤버 변수를 가져서는 안 된다.
  • 오버라이딩할 때는 virtualoverride 키워드를 사용한다.
  • 특정 플랫폼에 특화된 코드를 구현할 때는 일반 기능에 영향을 주지 않도록 설계한다.
profile
복습하기 위해 쓰는 글

2개의 댓글