futurelabunseen / B-JeonganLee

UNSEEN 2nd Term Learning and Project Repo.
5 stars 0 forks source link

2강: 언리얼 C++ 코딩 규칙 #11

Closed fkdl0048 closed 7 months ago

fkdl0048 commented 7 months ago

2강: 언리얼 C++ 코딩 규칙

코딩 표준이란?

코딩 표준이라는 것은 프로그래밍을 작성하는데 있어서 지켜야 하는 프로그래밍 이름 규칙, 작성 방법을 지정한 가이드 라인이다. 코딩 스타일, 코딩 컨벤션으로도 많이 한다.

앞서 이득우 교수님이 알아보라고 한, 언리얼 자체 컨베션에서도 같은 문장을 언급한다. (프린시플과 가이드라인)

언리얼 컨벤션 원칙 부분

좋은 코딩 표준

프로그래밍 영역에서 나쁨은 판단하기 쉽지만, 좋은 것에 대한 판단은 항상 어렵다. 코딩 표준도 마찬가지로 절대적으로 좋은 코딩 표준이란 없다. 상황과 환경에 따라 적절한 코딩 표준이 존재한다. 예를 들어 해당 팀의 컨벤션, 프로젝트 컨벤션에 맞춰서 사용하는 것이 더 바람직하다.

마치 한 사람이 짠것과 같이 느껴지는 것이 중요하다.

언리얼 컨벤션 가이드 중 일부 발췌.
스타일 가이드를 준수하여 매번 재학습이 아닌 같은 추상화 모델/메타 모델을 공유하는 것을 목적으로 한다. 스타일에 대한 청크가 없어지고 생산적인 생성 및 유지가 가능해진다.
스타일 가이드에서 어긋난다면 이를 바로잡도록 노력해야 한다. 혹시 주위에 있다면 친절하게 알려줄 것.
스타일 가이드가 없는 팀은 팀이 아니다. 법을 어기지 말것

언리얼엔진의 관점에서도 마찬가지로 엔진의 자체적인 코딩 표준이 있기 때문에 기존 C++코딩 방법을 버리고 새로운 표준을 따라가야 함 (권고사항이 아닌 규칙임)

언리얼 코딩 표준

지켜야 하는 이유

클래스 체계

클래스 는 작성자보다는 읽는 사람을 염두에 두고 조직되어야 합니다. 읽는 사람은 대부분 클래스의 퍼블릭 인터페이스를 사용할 것이므로, 퍼블릭 구현을 먼저 선언한 후 클래스의 프라이빗 구현이 뒤따라야 합니다.

즉, 읽는 사람은 퍼블릭 인터페이스만 읽고 사용할 가능성이 매우 높기 때문에 그에 대한 가독성과 비용을 줄이는 전략이다.

    UCLASS()
    class EXAMPLEPROJECT_API AExampleActor : public AActor
    {
        GENERATED_BODY()

    public: 
        // 이 액터 프로퍼티의 디폴트값 설정
        AExampleActor();

    protected:

              // 게임 시작 시 또는 스폰 시 호출
        virtual void BeginPlay() override;

    };

저작권 고지

공개 배포용으로 에픽에서 제공한 모든 소스 파일(.h , .cpp , .xaml )은 파일 첫 번째 줄에 저작권 고지를 포함해야 합니다. 저작권 고지의 포맷은 다음과 정확히 일치해야 합니다.

// Copyright Epic Games, Inc. All Rights Reserved.

이 줄이 누락되거나 올바른 양식으로 작성되지 않을 경우 CIS에서 오류를 생성하고 실패합니다.

명명 규칙

언리얼의 명명 규칙 정리 문서

언리얼은 대부분 파스칼 케이스를 사용한다.

탬플릿 클래스

템플릿 클래스에는 접두사 T를 포함합니다.

class TAttribute

UObject

UObject에서 상속하는 클래스에는 접두사 U를 포함합니다.

class UActorComponent

AActor

AActor에서 상속하는 클래스에는 접두사 A를 포함합니다.

class AActor

SWidget

SWidget에서 상속하는 클래스에는 접두사 S를 포함합니다.

class SCompoundWidget

추상적 인터페이스

추상적 인터페이스인 클래스에는 접두사 I를 포함합니다.

class IAnalyticsProvider

열거형

열거형에는 접두사 E를 포함합니다.

enum class EColorBits
{
   ECB_Red,
   ECB_Green,
   ECB_Blue
};

불리언 변수

부울 변수에는 접두사 b를 포함합니다.

bPendingDestruction

bHasFadedIn.

그 외 클래스

그 외 대부분의 클래스는 접두사 F를 포함합니다. 그러나 일부 서브시스템은 다른 글자를 사용하기도 합니다.

Typedef의 경우 다음과 같이 해당 타입에 적합한 접두사를 사용합니다.

특정 템플릿 인스턴스화의 typedef는 더 이상 템플릿이 아니며, 알맞은 접두사를 붙여야 합니다.

typedef TArray<FMytype> FArrayOfMyTypes;

추가적인 네이밍 규칙

포용적 단어 선택

언리얼 엔진 코드베이스에서 작업할 때는 늘 정중하고, 포용적이며, 전문적인 언어를 사용하기 위해 노력하는 것이 좋습니다.

조심할 것

포터블 C++ 코드

서버에서 전송을 필요로 할 때, int8을 많이 사용하고 일반적으로 int32을 사용한다. 자료형 자체도 많이 다른 모습이다.

표준 라이브러리 사용

과거에는 C++표준 라이브러리를 사용하지 않았지만, 지금은 안정성이 높아져서 잘 사용한다. (실제로 언리얼측에선 UE라이브러리 개발을 중단하고 표준 라이브러리의 이주를 생각 중이라고 한다.)

하지만, 표준 라이브러리를 말 그대로 범용적인 라이브러리이기 때문에 복잡도가 높아질 수 있다.

코멘트

const 정확도

const로 선언된 변수는 불변하다라는 것을 지시하는 것.

개인적으로 C#과 다르게 이렇게 불변을 보장하는 것을 개발자에게 직접적으로 알려주거나 지정할 수 있는 점이 마음에 들었다. C#은 readonly나 const 또는 레코드를 사용하여 컴파일 또는 생성자단위에서만 1회성 불변이 가능했다면, C++의 경우 매개변수, 함수 단위로 불변을 조작할 수 있다는 점이 신기하다. 아마 C레벨의 언어라 메모리접근 자체를 조작하는 이유라 그런 듯 하다.

    void SomeMutatingOperation(FThing& OutResult, const TArray<Int32>& InArray)
    {
        // InArray는 여기서 수정되지 않지만, OutResult는 수정될 수도 있습니다.
    }

    void FThing::SomeNonMutatingOperation() const
    {
        // 이 코드는 자신을 호출한 FThing을 수정하지 않습니다.
    }

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

궁금증, 탬플릿을 const로 제한한다고 해서 그 탬플릿 자체의 불변을 보장하지 못한다고 알고 있는데, 언리얼 C++에선 가능한건지 궁금..

예시 포맷

자동으로 문서를 만들어주는? JavaDoc이라는 시스템을 이용중이라고 한다. 이를 위해 포맷 규칙을 따르는 것이 좋다고 한다. 개인적으로 궁금한 부분이라 나중에 더 찾아볼 예정

최신 C++ 언어 문법

C++20버전으로 빌드하며 최소 조건은 C++17이라고 한다. 최신 문법에 대한 트레이드 오프는 마찬가지로 해당 팀, 프로젝트에 따라 다르다.

스태틱 어서트

static_assert 키워드는 컴파일 시간 어서트가 필요한 경우 사용할 수 있습니다.

Override 및 Final

override 및 final 키워드는 사용할 수 있을 뿐만 아니라, 사용을 강력히 권합니다. 빠진 부분이 다수 있을 수 있으나, 서서히 수정될 예정입니다.

Nullptr

nullptr 은 모든 경우 C 스타일 NULL 매크로 대신 사용해야 합니다.

Auto

아래 몇 가지 예외를 제외하면 C++ 코드에서 auto 를 사용해서는 안 됩니다. 초기화하려는 타입은 항상 명시해 주어야 합니다. 즉, 읽는 사람에게 타입이 명확하게 보여야 한다는 뜻입니다. 이 규칙은 C#의 var 키워드 사용에도 적용됩니다.

C#에서는 var키워드 사용을 권장하는 반면, 언리얼 C++는 강력한 코딩 규칙으로 이를 제한하는 것 같다. C#의 var의 경우 동적 추론이 대부분 가능하지만, 언리얼 C++는 네이밍 규칙이 명확하기 때문에 다른 문서에서도 언급하지만, 컨텍스트로 해석을 강조하는 걸 봐선 변수와 키워드등이 한줄로 명확하게 읽는 것을 원하는 것 같다.

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

foreach문으로 생각하면 될 것 같다.

람다 및 익명 함수

람다는 자유롭게 사용할 수 있습니다. 람다를 최적으로 사용하려면 길이상 두 구문 정도가 되어야 합니다. 특히 규모가 더 큰 표현식이나 구문의 일부로 사용될 때, 예를 들면 범용 알고리즘의 술부(predicate)에 사용될 때는 더욱 그렇습니다.

// 이름에 단어 '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(); });|

스테이트풀 람다는 자주 사용하는 경향이 있는 함수 포인터에 할당할 수 없다는 점에 유의하세요. 사소하지 않은 람다는 일반 함수와 같은 방식으로 문서화해야 합니다.

람다도 C#에선 많이 사용했는데, 어떤 부분이 다른지 좀 알아보니 =>C#과 다르게 [](매개변수 목록) -> 반환타입 {함수 몸체}형태로 작성된다고 한다. 차이점은 간결한 C#과 캡쳐목록과 반환타입을 명시하는 유연성이 있는 C++이다.

열거형

열거형(Enumerated, Enum) 클래스는 기존 네임스페이스 열거형인 일반 열거형 및 UENUM 을 대체합니다. 예시는 다음과 같습니다.

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

    // 새 열거형
    UENUM()
    enum class EThing : uint8
    {
        Thing1,
        Thing2
    }

위 열거형은 기존 언리얼에서 사용하던 Enum형태이지만 현재는 아래와 같이 사용한다. 그렇지만 위 방식도 알고 있어야 함 (이미 개발된 코드가 많기 때문)

이동 시맨틱

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;
    };

서드 파티 코드

주석으로 명확하게 표시를 하자

코드 포맷

중괄호

중괄호에 대한 논쟁은 오랫동안 이어져 왔습니다. 에픽에서는 새 줄에 중괄호를 사용하는 것이 오래된 관행처럼 이어지고 있으니, 이를 준수하여 주시기 바랍니다.

다음 예시와 같이 단일 구문 블록에도 항상 중괄호를 포함시켜 주세요.

if (bThing)
{
    return;
}

C#과 똑같은 중괄호 포맷, C++과는 다르다.

탭 및 들여쓰기

똑같음

Switch 문

디폴트 케이스는 항상 만들어 두고, 다른 사람이 그 뒤에 새로운 케이스를 추가할 때에 대비해 break도 넣어 두시기 바랍니다.

네임스페이스

게임코드에서는 직접적으로 사용할 일이 없지만, 에디터단위에서 사용한다면 자주 사용하게 됨

물리적 종속성

파일 이름에는 가급적 접두사를 붙이지 않아야 합니다.

예를 들면 UScene.cpp 보다는 Scene.cpp 가 좋습니다. 이렇게 하면 원하는 파일을 식별하는 데 필요한 글자 수가 줄어들어 Workspace Whiz나 Visual Assist와 같은 툴에서 Open File in Solution 등의 기능을 쉽게 사용할 수 있습니다.

모든 헤더는 #pragma once 지시어(directive)로 복수의 include를 방지해야 합니다.

참고로 에픽이 사용하는 모든 컴파일러는 #pragma once 를 지원합니다.

정리하면 가능한 수준까지 적은 수의 헤더를 포함하는 것이 좋습니다. 헤더를 포함하는 것은 컴파일 시간을 늘리고, 불필요한 종속성을 만들어 낼 수 있습니다.

캡슐화

객체지향의 기본이자, 가장 중요한 부분

객체의 자율성을 보장할 수 있게 만들자

일반적인 스타일 문제

의존성을 잘 관리하자 (응집성은 높고, 결합도는 낮게)

//다음은 사용 가능합니다.
     FShaderType* Ptr

//다음은 사용해서는 안 됩니다.
    FShaderType *Ptr
    FShaderType * Ptr

API 디자인 가이드라인

정리