1790 words
9 minutes
[UE5/Part1] 2. UE C++ 표준 Docs

“이득우의 언리얼 프로그래밍 Part1 - 언리얼 C++의 이해” 학습 내용을 정리한 강의 노트입니다.
옵시디언에 정리한 마크다운 문서라 블로그 마크다운 양식에 일부 맞지 않을 수 있습니다.

이번 노트는 언리얼 표준 컨벤션을 요약 정리한 내용입니다.

강의 목표#

  • 프로그래밍을 시작하기 전에 알아두어야 할 언리얼 C++ 코딩 표준 이해
  • 언리얼 C++ 코딩 표준에서 주의할 점 확인

구글 & 언리얼 C++ 코딩 표준 URL#

클래스 체계#

public 구현을 먼저 선언한 후 클래스의 private 구현이 뒤따라야 한다.

명명 규칙#

  • 파스칼 케이싱 (Pascal Casing) 사용
  • U A S E b F 등의 접두어 활용
    • UObject U
    • AActor A
    • Swidget S : Slate 라는 UI전용 클래스
    • enum E
    • bool b : 소문자 - ex: bPendingDestrction
  • 그 외 대부분의 클래스는 접두사 F

포터블 C++ 코드 (ex: int)#

  • int 및 부호 없는 int타입은 플랫폼에 따라 크기가 다를 수 있다.
  • 주요 타입은 아래와 같이 크기를 표기해준다.
    • 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바이트)
    • PTRINT - 포인터를 가질 수 있는 정수(PTRINT 크기 추정 금지)

Const 정확도#

  • const 가능하면 붙여주기
  • 그 외에 아래와 같은 경우에도 const 활용

1. 루프#

TArray<FString> StringArray;
for (const FString& : StringArray)
{
    // 이 루프의 바디는 StringArray를 수정하지 않습니다.
}

2. 포인터 자체에 const#

	// 데이터 수정은 가능하나, 포인터에 증감 연산자 사용은 불가함
    T* const Ptr = ...;
    
    // X 틀림
    T& const Ref = ...;

3. 데이터에 const#

	// X [복사] 나쁜 예 - const 배열 반환 
    const TArray<FString> GetSomeArray();
    
    // [읽기] 좋은 예 - const 배열로의 레퍼런스 반환
    const TArray<FString>& GetSomeArray();
    
    // [읽기] 좋은 예- const 배열로의 포인터 반환
    const TArray<FString>* GetSomeArray();
    
    // X [나쁜 예 - const 배열로의 const 포인터 반환 (포인터/데이터 모두 const)
    const TArray<FString>* const GetSomeArray();

예시 포맷#

규칙에 맞춰 코드를 작성하면, JavaDoc기반으로 자동 문서화 가능

최신 C++ 문법#

  • 언리얼 엔진은 기본적으로 C++ 20 버전으로 컴파일
  • 아래에 지원되는 최신 C++ 컴파일러 기능으로 명시된 것 이외에는 신증하게 사용

static_assert#

static_assert 키워드는 컴파일 시간 조건 검증을 위해 사용 가능

  • 조건이 거짓이면 컴파일 자체를 실패, 잘못된 코드가 실행 파일로 만들어지지 않도록 차단
// 기본 문법
static_assert(조건식, "에러 메시지");
// 1. [타입 크기 검증] 네트워크 패킷은 반드시 64바이트여야 한다
struct FMyPacket
{
    int32 Data[16];
};

static_assert(sizeof(FMyPacket) == 64, "FMyPacket must be 64 bytes!");

// 2. [템플릿 타입 제약]
template<typename T>
void ProcessValue(T Value)
{
    static_assert(std::is_integral<T>::value, "T must be an integral type");
    // 정수형 타입만 허용
}

// 3. [엔진 내부 규칙 강제] UObject 파생 클래스만 허용
template<typename T>
class TMyManager
{
    static_assert(TIsDerivedFrom<T, UObject>::IsDerived, "T must derive from UObject");
};

override & final#

사용을 강력히 권합니다.

nullptr#

C 스타일의 NULL -> nullptr을 사용

auto#

auto는 왠만하면 사용하지 말기

  • 변수에 람다 바인딩
  • 이터레이터 타입이 장황해 가독성에 악영향을 끼칠때 auto it2 = names.begin();
  • (고급 ?) 템플릿 코드에서 표현식의 타입을 쉽게 식별 못할때

범위 기반 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);
    }

강 - 타입 Enum#

가능하면 enum class 를 사용

 // 기존 열거형
    UENUM()
    namespace EThing
    {
        enum Type
        {
            Thing1,
            Thing2
        };
    }
    
    // 새 열거형
    UENUM()
    enum class EThing : uint8
    {
        Thing1,
        Thing2
    }

디폴트 멤머 이니셜라이저#

class UTeaOptions : public UObject
{
    GENERATED_BODY()

public:
    UPROPERTY()
    int32 MaximumNumberOfCupsPerDay = 10;   // ← 여기서 바로 초기화
};

위와 같이 바로 초기화 하면, 생성자에서 멤버 초기화가 필요 없다.

  • 엔진 코드: 디폴트 멤머 이니셜라이저 자주 사용 X
  • 게임 코드: 개발자가 직접 관리하니, 가독성 & 편의성 측면에서 자주 사용 O
  • config INI환경설정 파일을 통해 관리 하는것도 권장

물리적 종속성 (Physical Dependencies)#

  1. 파일 이름 : 접두사 X UScene.cpp -> Scene.cpp
  2. 헤더 관리
    1. 모든 헤더에 #pragma once넣어 중복 include 방지
    2. 헤더가 늘어나면 컴파일 시간이 증가 (가능하면 적게 include)

캡슐화#

가능하면 private으로 선언, getter/setter로 접근

일반적인 스타일 문제#

  • 문자열은 꼭 TEXT() 매크로 사용
  • 포인터 선언은 무조건 FShaderType* Ptr 구조로
    • Find In Files검색에 용이
  • 헤더에 특수한 스태틱 변수 정의 X
// SomeModule.h
static const FString GUsefulNamedString = TEXT("String"); // 각 cpp파일에서 복사본이 생김

// 위 헤더를 참조하는 모든 인스턴스들이 컴파일 됨
// 아래와 같이 개선해야함

// SomeModule.h
extern SOMEMODULE_API const FString GUsefulNamedString; // 링크 단계에서 찾으라고 명시

// SomeModule.cpp
const FString GUsefulNamedString = TEXT("String");

API 디자인 가이드라인#

부울 함수 파라미터 피하기#

  • 호출하는 쪽에서 flase, ture가 각각 무엇을 의미하는지 알기 어려움
  • API 확장성이 부족함
MakeCupOfTea(Tea, false, true, true);
MakeCupOfTea(Tea, false, true, true, false, 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);

인터페이스 클래스 규칙#

  • 접두사 I 활용
  • 멤버 변수 X
  • 항상 추상형
// 리플렉션용 껍데기
UINTERFACE(MinimalAPI)
class UDrinkable : public UInterface
{
    GENERATED_BODY()
};

// 실제 인터페이스
class IDrinkable
{
public:
    virtual void Drink() = 0; // 반드시 구현해야 하는 계약
};


// 실제 구현 클래스 UCup
UCLASS()
class UCup : public UObject, public IDrinkable
{
    GENERATED_BODY()

public:
    virtual void Drink() override
    {
        UE_LOG(LogTemp, Log, TEXT("Cup is being drunk!"));
    }
};

정리#

  1. public 에서 private로 이어지는 클래스 체계 (Organization) 준수
  2. 명명 규칙
    1. 파스칼 케이싱(Pascal Casing)
    2. 소문자를 가급적 사용하지 말고, 공백 및 언더스코어 없음
    3. 모든 클래스와 구조체에는 고유 접두사
  3. 코드의 명확성
    1. 파라미터에 가급적 In Out 접두사
    2. const 지시자 (directive) 적극적 활용
    3. 레퍼런스를 통한 복사 방지
    4. auto 키워드 가급적 자제
  4. Find In Files 활용
  5. 헤더 파일 및 #include구문 의존성 최소화 시켜 주의 깊게 다루기