프로그래밍 영역에서 나쁨은 판단하기 쉽지만, 좋은 것에 대한 판단은 항상 어렵다. 코딩 표준도 마찬가지로 절대적으로 좋은 코딩 표준이란 없다. 상황과 환경에 따라 적절한 코딩 표준이 존재한다. 예를 들어 해당 팀의 컨벤션, 프로젝트 컨벤션에 맞춰서 사용하는 것이 더 바람직하다.
마치 한 사람이 짠것과 같이 느껴지는 것이 중요하다.
언리얼 컨벤션 가이드 중 일부 발췌.
스타일 가이드를 준수하여 매번 재학습이 아닌 같은 추상화 모델/메타 모델을 공유하는 것을 목적으로 한다. 스타일에 대한 청크가 없어지고 생산적인 생성 및 유지가 가능해진다.
스타일 가이드에서 어긋난다면 이를 바로잡도록 노력해야 한다. 혹시 주위에 있다면 친절하게 알려줄 것.
스타일 가이드가 없는 팀은 팀이 아니다. 법을 어기지 말것
언리얼엔진의 관점에서도 마찬가지로 엔진의 자체적인 코딩 표준이 있기 때문에 기존 C++코딩 방법을 버리고 새로운 표준을 따라가야 함 (권고사항이 아닌 규칙임)
언리얼 코딩 표준
지켜야 하는 이유
소프트웨어의 총 수명 비용 중 80%는 유지보수에 소모됩니다.
최초 작성자가 그 소프트웨어의 수명이 다할 때까지 유지보수하는 경우는 거의 없습니다.
코딩 규칙을 사용하면 소프트웨어의 가독성을 향상하여 엔지니어가 새로운 코드를 빠르고 완벽히 이해할 수 있습니다.
에픽에서 소스 코드를 모드 개발자 커뮤니티에 공개할 경우 코딩 규칙을 알고 있으면 이해하기 더 쉽습니다.
대다수의 코딩 규칙이 크로스 컴파일러 호환성에 필요합니다.
클래스 체계
클래스 는 작성자보다는 읽는 사람을 염두에 두고 조직되어야 합니다. 읽는 사람은 대부분 클래스의 퍼블릭 인터페이스를 사용할 것이므로, 퍼블릭 구현을 먼저 선언한 후 클래스의 프라이빗 구현이 뒤따라야 합니다.
즉, 읽는 사람은 퍼블릭 인터페이스만 읽고 사용할 가능성이 매우 높기 때문에 그에 대한 가독성과 비용을 줄이는 전략이다.
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에서 오류를 생성하고 실패합니다.
enum class EColorBits
{
ECB_Red,
ECB_Green,
ECB_Blue
};
불리언 변수
부울 변수에는 접두사 b를 포함합니다.
bPendingDestruction
bHasFadedIn.
그 외 클래스
그 외 대부분의 클래스는 접두사 F를 포함합니다. 그러나 일부 서브시스템은 다른 글자를 사용하기도 합니다.
Typedef의 경우 다음과 같이 해당 타입에 적합한 접두사를 사용합니다.
구조체의 typedef인 경우 F
UObject 의 typedef인 경우 U
특정 템플릿 인스턴스화의 typedef는 더 이상 템플릿이 아니며, 알맞은 접두사를 붙여야 합니다.
typedef TArray<FMytype> FArrayOfMyTypes;
C#에서는 접두사가 생략됩니다.
언리얼 헤더 툴의 경우 대부분 올바른 접두사가 필요하므로, 접두사를 제공하는 것이 중요합니다.
추가적인 네이밍 규칙
타입 및 변수 이름은 명사입니다.
메서드 이름은 메서드의 이펙트를 설명하거나, 이펙트가 없는 메서드의 반환 값을 설명하는 동사입니다.
매크로 이름은 모두 대문자로 구성되고, 단어가 언더스코어로 분리되며, 접두사 UE_ 가 사용되어야 합니다.
#define UE_AUDIT_SPRITER_IMPORT
변수, 메서드, 클래스 이름은 명확함, 확실함, 내용을 파악할 수 있음을 기본으로 한다.
명확하지 않거나 모호한 경우, and와 같은 둘 이상의 책임이 있는 경우 SRP을 어긴것으로 분리를 생각
부울을 반환하는 모든 함수는 IsVisible() 또는 ShouldClearBuffer() 등의 true/false 질문을 해야 합니다.
프로시저(반환 값이 없는 함수)는 강한 동사 뒤에 오브젝트를 붙여 써야 합니다. 메서드의 오브젝트가 그 안에 있는 오브젝트일 때는 예외이며, 이 경우 오브젝트는 컨텍스트에서 이해됩니다. 'Handle' 및 'Process' 등의 모호한 동사로 시작하는 이름은 피해야 합니다.
즉, 해당 객체 이름으로 해석이 가능한 영역에서 모호하지 않고 명확한 이름을 사용해야 한다.
다음과 같은 경우 함수 파라미터 이름에 접두사 'Out'을 추가할 것이 좋습니다.
함수 파라미터가 레퍼런스로 전달되는 경우
함수를 그 값에 쓸 것으로 예상되는 경우
포용적 단어 선택
언리얼 엔진 코드베이스에서 작업할 때는 늘 정중하고, 포용적이며, 전문적인 언어를 사용하기 위해 노력하는 것이 좋습니다.
조심할 것
포터블 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 크기 추정 금지)
서버에서 전송을 필요로 할 때, int8을 많이 사용하고 일반적으로 int32을 사용한다. 자료형 자체도 많이 다른 모습이다.
표준 라이브러리 사용
과거에는 C++표준 라이브러리를 사용하지 않았지만, 지금은 안정성이 높아져서 잘 사용한다. (실제로 언리얼측에선 UE라이브러리 개발을 중단하고 표준 라이브러리의 이주를 생각 중이라고 한다.)
하지만, 표준 라이브러리를 말 그대로 범용적인 라이브러리이기 때문에 복잡도가 높아질 수 있다.
코멘트
언리얼 엔진을 떠나서 기본적인 주석의 올바른 형태 (클린코드)
const 정확도
const로 선언된 변수는 불변하다라는 것을 지시하는 것.
개인적으로 C#과 다르게 이렇게 불변을 보장하는 것을 개발자에게 직접적으로 알려주거나 지정할 수 있는 점이 마음에 들었다. C#은 readonly나 const 또는 레코드를 사용하여 컴파일 또는 생성자단위에서만 1회성 불변이 가능했다면, C++의 경우 매개변수, 함수 단위로 불변을 조작할 수 있다는 점이 신기하다. 아마 C레벨의 언어라 메모리접근 자체를 조작하는 이유라 그런 듯 하다.
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로 제한한다고 해서 그 탬플릿 자체의 불변을 보장하지 못한다고 알고 있는데, 언리얼 C++에선 가능한건지 궁금..
예시 포맷
자동으로 문서를 만들어주는? JavaDoc이라는 시스템을 이용중이라고 한다. 이를 위해 포맷 규칙을 따르는 것이 좋다고 한다. 개인적으로 궁금한 부분이라 나중에 더 찾아볼 예정
최신 C++ 언어 문법
C++20버전으로 빌드하며 최소 조건은 C++17이라고 한다. 최신 문법에 대한 트레이드 오프는 마찬가지로 해당 팀, 프로젝트에 따라 다르다.
스태틱 어서트
static_assert 키워드는 컴파일 시간 어서트가 필요한 경우 사용할 수 있습니다.
Override 및 Final
override 및 final 키워드는 사용할 수 있을 뿐만 아니라, 사용을 강력히 권합니다. 빠진 부분이 다수 있을 수 있으나, 서서히 수정될 예정입니다.
Nullptr
nullptr 은 모든 경우 C 스타일 NULL 매크로 대신 사용해야 합니다.
Auto
아래 몇 가지 예외를 제외하면 C++ 코드에서 auto 를 사용해서는 안 됩니다. 초기화하려는 타입은 항상 명시해 주어야 합니다. 즉, 읽는 사람에게 타입이 명확하게 보여야 한다는 뜻입니다. 이 규칙은 C#의 var 키워드 사용에도 적용됩니다.
auto 를 사용 가능한 경우는 다음과 같습니다.
변수에 람다를 바인딩해야 하는 경우. 람다 타입은 코드로 표현할 수 없기 때문입니다.
이터레이터 변수의 경우. 단, 이터레이터 타입이 매우 장황하여 가독성에 악영향을 미치는 경우에 한합니다.
템플릿 코드에서 표현식의 타입을 쉽게 식별할 수 없는 경우. 고급 사용 사례입니다.
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 사용법은 아직도 확립 중이지만, 최적화된 코드베이스 영역 일부에서는 이미 찾아볼 수 있습니다.
Move를 쓰지말고 MoveTemp를 쓰라
디폴트 멤버 이니셜라이저
기본값 초기화
디폴트 멤버 이니셜라이저는 클래스 자체 내에서 클래스 디폴트값을 정의하는 데 사용할 수 있습니다.
중괄호에 대한 논쟁은 오랫동안 이어져 왔습니다. 에픽에서는 새 줄에 중괄호를 사용하는 것이 오래된 관행처럼 이어지고 있으니, 이를 준수하여 주시기 바랍니다.
다음 예시와 같이 단일 구문 블록에도 항상 중괄호를 포함시켜 주세요.
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 를 지원합니다.
헤더 include 대신 전방 선언(forward declaration)이 가능한 경우 그렇게 합니다.
대부분 전방선언으로
정리하면 가능한 수준까지 적은 수의 헤더를 포함하는 것이 좋습니다. 헤더를 포함하는 것은 컴파일 시간을 늘리고, 불필요한 종속성을 만들어 낼 수 있습니다.
캡슐화
객체지향의 기본이자, 가장 중요한 부분
객체의 자율성을 보장할 수 있게 만들자
일반적인 스타일 문제
의존성을 잘 관리하자 (응집성은 높고, 결합도는 낮게)
종속성 거리를 최소화하세요.
코드가 특정 값을 갖는 변수에 의존할 때는, 변수를 사용하기 직전에 그 값을 설정합니다. 실행 블록 상단에 변수 값을 초기화한 상태로 코드 수백 줄 동안 사용하지 않는다면, 그 종속성을 모르는 사람이 실수로 그 값을 바꾸게 될 가능성이 높습니다. 바로 다음 줄에 사용한다면 변수 초기화를 왜 그렇게 했는지, 어디서 사용되는지를 명확히 할 수 있습니다.
메서드는 가급적 하위 메서드로 분할하세요.
세밀한 부분부터 시작해서 큰 그림을 재구성하기보다는, 큰 그림을 먼저 그린 후 필요한 세밀한 부분을 자세히 살펴보는 것이 더 쉬울 수도 있습니다. 마찬가지로, 모든 코드가 통째로 들어 있는 메서드보다는 이름을 잘 지어 둔 다수의 하위 메서드를 연속적으로 호출하는 단순한 메서드를 이해하는 것이 더 수월합니다.
함수 선언이나 함수 호출 위치에서 함수의 이름과 실행인자 목록에 선행되는 괄호 사이에 스페이스를 추가하지 마세요.
컴파일러 경고에 주의를 기울여 주세요.
컴파일러 경고 메시지는 무언가 잘못되었다는 것을 뜻하므로 컴파일러가 경고하는 내용을 고쳐야 합니다. 전혀 처리할 수 없다면 #pragma 로 억제할 수는 있지만, 이는 최후의 수단이어야 합니다.
파일 끝에 빈 줄 하나를 만드세요.
모든 .cpp 및 .h 파일은 빈 줄이 있어야 gcc와 함께 제대로 작동합니다.
디버그 코드는 전반적으로 유용하고 잘 다듬어진 상태가 아니라면 체크인하지 말아야 합니다.
디버그 코드가 다른 코드와 섞이면 다른 코드를 읽기가 힘들어집니다.
스트링 리터럴 주변에는 항상 TEXT() 매크로를 사용하세요.
TEXT() 매크로가 없으면 코드가 리터럴에서 FStrings 를 생성하는 경우 원치 않는 스트링 변환 프로세스가 유발됩니다.
루프에서의 동일 연산 반복을 피하세요.
공통된 하위 표현식은 루프 밖으로 빼서 중복 계산을 피합니다. 경우에 따라 statics를 활용하여 전역 범위에서의 함수 호출을 대상으로 하는 중복 연산을 피할 수 있는데, 스트링 리터럴에서의 FName 생성 등을 예로 들 수 있습니다.
핫 리로드 기능을 염두에 두세요.
종속성을 최소화하여 반복작업 시간을 줄입니다. 리로드 동안 변할 확률이 있는 함수에는 인라인 또는 템플릿을 사용하지 않습니다. 리로드 동안 그대로 남아 있을 것에만 statics를 사용하시기 바랍니다.
복잡한 표현식은 중간 변수를 사용하여 간소화하세요.
복잡한 표현식을 중간 변수에 할당된 하위 표현식으로 나누고, 부모 표현식 내에서 하위 표현식의 의미를 설명하는 이름을 지정하면 이해하기 더 쉬워집니다. 예시는 다음과 같습니다.
포인터와 레퍼런스의 스페이스는 그 오른쪽에 딱 한 칸만 두어야 합니다.
그래야 특정 타입에 대한 모든 포인터나 레퍼런스에 빠르게 Find in Files 를 사용할 수 있습니다. 예시는 다음과 같습니다.
Find in Files: 검색할 때 유리함
//다음은 사용 가능합니다.
FShaderType* Ptr
//다음은 사용해서는 안 됩니다.
FShaderType *Ptr
FShaderType * Ptr
섀도잉된 변수는 허용되지 않습니다.
C++에서는 외부 영역에서의 변수를 섀도잉하는 것이 가능하지만, 이는 읽는 사람에게 모호할 수 있습니다. 예를 들어, 다음 멤버 함수에서 Count 변수는 세 가지 방법으로 사용할 수 있습니다.
함수 호출에서 익명 리터럴 사용은 피하세요.
명명된 상수로 의미를 설명하는 것이 좋습니다. 이렇게 하면 함수 선언을 조회하지 않아도 이해할 수 있으므로 일반적인 독자가 의도를 쉽게 파악할 수 있습니다.
헤더에 특수한 스태틱 변수를 정의하지 않도록 합니다.
특수한 스태틱 변수는 해당 헤더가 포함된 이동 단위마다 인스턴스를 컴파일합니다.
헤더에서 정의하고, cpp에서 초기화 및 구현을 하는 것이 좋다.
API 디자인 가이드라인
부울 함수 파라미터는 피해야 합니다.
특히 함수에 전달되는 플래그의 경우 부울 파라미터를 피해야 합니다. 앞서 언급한 익명 리터럴 문제가 그대로 발생합니다. 또한 시간에 따라 API 확장을 통해 동작이 추가되면서 늘어나는 경향도 있습니다. 대신 다음과 같이 열거형을 사용하는 것이 좋습니다. (강 - 타입 Enum 섹션에서 열거형을 플래그로 사용하는 것에 대한 조언을 참조하세요)
너무 긴 함수 파라미터 목록은 피하세요.
함수가 파라미터를 많이 받는 경우 다음과 같이 전용 구조체 전달을 고려해 보세요.
애초에 너무 많은 파라미터를 받는 것을 피하는 것이 좋다. 그 함수가 너무 많은 일을 하고 있는 것이다. (분리할 것)
인터페이스 클래스는 항상 추상형이어야 합니다.
인터페이스 클래스는 접두사 'I'를 포함하며, 멤버 변수가 있어서는 안 됩니다. 인터페이스는 순수 가상(pure virtual)이 아닌 메서드를 포함할 수 있으며, 인라인 구현되는 한 가상이 아니거나 정적인 메서드도 포함할 수 있습니다.
C++는 인터페이스 기능을 흉내내기 때문에 멤버 변수나 구현이 내부에 존재할 수 있는데, 그러지 말고 인터페이스 답게 사용하라
2강: 언리얼 C++ 코딩 규칙
강의 목표
강의 과제
작업 Issue
언리얼 5.3 코딩 표준 공식 문서
코딩 표준이란?
코딩 표준이라는 것은 프로그래밍을 작성하는데 있어서 지켜야 하는 프로그래밍 이름 규칙, 작성 방법을 지정한 가이드 라인이다.
코딩 스타일
,코딩 컨벤션
으로도 많이 한다.앞서 이득우 교수님이 알아보라고 한, 언리얼 자체 컨베션에서도 같은 문장을 언급한다. (프린시플과 가이드라인)
언리얼 컨벤션 원칙 부분
좋은 코딩 표준
프로그래밍 영역에서 나쁨은 판단하기 쉽지만, 좋은 것에 대한 판단은 항상 어렵다. 코딩 표준도 마찬가지로 절대적으로 좋은 코딩 표준이란 없다. 상황과 환경에 따라 적절한 코딩 표준이 존재한다. 예를 들어 해당 팀의 컨벤션, 프로젝트 컨벤션에 맞춰서 사용하는 것이 더 바람직하다.
마치 한 사람이 짠것과 같이 느껴지는 것이 중요하다.
언리얼엔진의 관점에서도 마찬가지로 엔진의 자체적인 코딩 표준이 있기 때문에 기존
C++
코딩 방법을 버리고 새로운 표준을 따라가야 함 (권고사항이 아닌 규칙임)언리얼 코딩 표준
지켜야 하는 이유
클래스 체계
클래스 는 작성자보다는 읽는 사람을 염두에 두고 조직되어야 합니다. 읽는 사람은 대부분 클래스의 퍼블릭 인터페이스를 사용할 것이므로, 퍼블릭 구현을 먼저 선언한 후 클래스의 프라이빗 구현이 뒤따라야 합니다.
즉, 읽는 사람은 퍼블릭 인터페이스만 읽고 사용할 가능성이 매우 높기 때문에 그에 대한 가독성과 비용을 줄이는 전략이다.
저작권 고지
공개 배포용으로 에픽에서 제공한 모든 소스 파일(.h , .cpp , .xaml )은 파일 첫 번째 줄에 저작권 고지를 포함해야 합니다. 저작권 고지의 포맷은 다음과 정확히 일치해야 합니다.
이 줄이 누락되거나 올바른 양식으로 작성되지 않을 경우 CIS에서 오류를 생성하고 실패합니다.
명명 규칙
언리얼의 명명 규칙 정리 문서
언리얼은 대부분 파스칼 케이스를 사용한다.
탬플릿 클래스
템플릿 클래스에는 접두사 T를 포함합니다.
UObject
UObject에서 상속하는 클래스에는 접두사 U를 포함합니다.
AActor
AActor에서 상속하는 클래스에는 접두사 A를 포함합니다.
SWidget
SWidget에서 상속하는 클래스에는 접두사 S를 포함합니다.
추상적 인터페이스
추상적 인터페이스인 클래스에는 접두사 I를 포함합니다.
열거형
열거형에는 접두사 E를 포함합니다.
불리언 변수
부울 변수에는 접두사 b를 포함합니다.
그 외 클래스
그 외 대부분의 클래스는 접두사 F를 포함합니다. 그러나 일부 서브시스템은 다른 글자를 사용하기도 합니다.
Typedef의 경우 다음과 같이 해당 타입에 적합한 접두사를 사용합니다.
특정 템플릿 인스턴스화의 typedef는 더 이상 템플릿이 아니며, 알맞은 접두사를 붙여야 합니다.
추가적인 네이밍 규칙
타입 및 변수 이름은 명사입니다.
메서드 이름은 메서드의 이펙트를 설명하거나, 이펙트가 없는 메서드의 반환 값을 설명하는 동사입니다.
매크로 이름은 모두 대문자로 구성되고, 단어가 언더스코어로 분리되며, 접두사 UE_ 가 사용되어야 합니다.
변수, 메서드, 클래스 이름은
명확함
,확실함
,내용을 파악할 수 있음
을 기본으로 한다.SRP
을 어긴것으로 분리를 생각부울을 반환하는 모든 함수는
IsVisible()
또는ShouldClearBuffer()
등의 true/false 질문을 해야 합니다.프로시저(반환 값이 없는 함수)는 강한 동사 뒤에 오브젝트를 붙여 써야 합니다. 메서드의 오브젝트가 그 안에 있는 오브젝트일 때는 예외이며, 이 경우 오브젝트는 컨텍스트에서 이해됩니다. 'Handle' 및 'Process' 등의 모호한 동사로 시작하는 이름은 피해야 합니다.
다음과 같은 경우 함수 파라미터 이름에 접두사 'Out'을 추가할 것이 좋습니다.
포용적 단어 선택
언리얼 엔진 코드베이스에서 작업할 때는 늘 정중하고, 포용적이며, 전문적인 언어를 사용하기 위해 노력하는 것이 좋습니다.
조심할 것
포터블 C++ 코드
서버에서 전송을 필요로 할 때, int8을 많이 사용하고 일반적으로 int32을 사용한다. 자료형 자체도 많이 다른 모습이다.
표준 라이브러리 사용
과거에는
C++
표준 라이브러리를 사용하지 않았지만, 지금은 안정성이 높아져서 잘 사용한다. (실제로 언리얼측에선 UE라이브러리 개발을 중단하고 표준 라이브러리의 이주를 생각 중이라고 한다.)하지만, 표준 라이브러리를 말 그대로 범용적인 라이브러리이기 때문에 복잡도가 높아질 수 있다.
코멘트
const 정확도
const로 선언된 변수는 불변하다라는 것을 지시하는 것.
개인적으로
C#
과 다르게 이렇게 불변을 보장하는 것을 개발자에게 직접적으로 알려주거나 지정할 수 있는 점이 마음에 들었다.C#
은 readonly나 const 또는 레코드를 사용하여 컴파일 또는 생성자단위에서만 1회성 불변이 가능했다면,C++
의 경우 매개변수, 함수 단위로 불변을 조작할 수 있다는 점이 신기하다. 아마 C레벨의 언어라 메모리접근 자체를 조작하는 이유라 그런 듯 하다.궁금증, 탬플릿을 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 필드가 되었음에 유의하세요.
foreach문으로 생각하면 될 것 같다.
람다 및 익명 함수
람다는 자유롭게 사용할 수 있습니다. 람다를 최적으로 사용하려면 길이상 두 구문 정도가 되어야 합니다. 특히 규모가 더 큰 표현식이나 구문의 일부로 사용될 때, 예를 들면 범용 알고리즘의 술부(predicate)에 사용될 때는 더욱 그렇습니다.
스테이트풀 람다는 자주 사용하는 경향이 있는 함수 포인터에 할당할 수 없다는 점에 유의하세요. 사소하지 않은 람다는 일반 함수와 같은 방식으로 문서화해야 합니다.
람다도
C#
에선 많이 사용했는데, 어떤 부분이 다른지 좀 알아보니=>
인C#
과 다르게[](매개변수 목록) -> 반환타입 {함수 몸체}
형태로 작성된다고 한다. 차이점은 간결한C#
과 캡쳐목록과 반환타입을 명시하는 유연성이 있는C++
이다.열거형
열거형(Enumerated, Enum) 클래스는 기존 네임스페이스 열거형인 일반 열거형 및 UENUM 을 대체합니다. 예시는 다음과 같습니다.
위 열거형은 기존 언리얼에서 사용하던
Enum
형태이지만 현재는 아래와 같이 사용한다. 그렇지만 위 방식도 알고 있어야 함 (이미 개발된 코드가 많기 때문)이동 시맨틱
TArray , TMap , TSet , FString 과 같은 모든 주요 컨테이너 타입에는 move 컨스트럭터와 move 할당 연산자가 있습니다. 이러한 타입을 값으로 전달 또는 반환할 때 종종 자동으로 사용되지만, std::move 의 UE 해당 버전인 MoveTemp 를 통해 명시적으로 호출할 수도 있습니다.
값으로 컨테이너나 스트링을 반환하는 것은 보통 임시로 복사하는 비용이 없어 표현성에 유용하게 작용할 수 있습니다. 값 전달 관련 규칙 및 MoveTemp 사용법은 아직도 확립 중이지만, 최적화된 코드베이스 영역 일부에서는 이미 찾아볼 수 있습니다.
디폴트 멤버 이니셜라이저
기본값 초기화
디폴트 멤버 이니셜라이저는 클래스 자체 내에서 클래스 디폴트값을 정의하는 데 사용할 수 있습니다.
서드 파티 코드
주석으로 명확하게 표시를 하자
코드 포맷
중괄호
중괄호에 대한 논쟁은 오랫동안 이어져 왔습니다. 에픽에서는 새 줄에 중괄호를 사용하는 것이 오래된 관행처럼 이어지고 있으니, 이를 준수하여 주시기 바랍니다.
다음 예시와 같이 단일 구문 블록에도 항상 중괄호를 포함시켜 주세요.
C#
과 똑같은 중괄호 포맷, C++과는 다르다.탭 및 들여쓰기
똑같음
Switch 문
디폴트 케이스는 항상 만들어 두고, 다른 사람이 그 뒤에 새로운 케이스를 추가할 때에 대비해 break도 넣어 두시기 바랍니다.
네임스페이스
게임코드에서는 직접적으로 사용할 일이 없지만, 에디터단위에서 사용한다면 자주 사용하게 됨
물리적 종속성
파일 이름에는 가급적 접두사를 붙이지 않아야 합니다.
예를 들면 UScene.cpp 보다는 Scene.cpp 가 좋습니다. 이렇게 하면 원하는 파일을 식별하는 데 필요한 글자 수가 줄어들어 Workspace Whiz나 Visual Assist와 같은 툴에서 Open File in Solution 등의 기능을 쉽게 사용할 수 있습니다.
모든 헤더는 #pragma once 지시어(directive)로 복수의 include를 방지해야 합니다.
참고로 에픽이 사용하는 모든 컴파일러는 #pragma once 를 지원합니다.
정리하면 가능한 수준까지 적은 수의 헤더를 포함하는 것이 좋습니다. 헤더를 포함하는 것은 컴파일 시간을 늘리고, 불필요한 종속성을 만들어 낼 수 있습니다.
캡슐화
객체지향의 기본이자, 가장 중요한 부분
객체의 자율성을 보장할 수 있게 만들자
일반적인 스타일 문제
의존성을 잘 관리하자 (응집성은 높고, 결합도는 낮게)
TEXT()
매크로를 사용하세요.API 디자인 가이드라인
C++
는 인터페이스 기능을 흉내내기 때문에 멤버 변수나 구현이 내부에 존재할 수 있는데, 그러지 말고 인터페이스 답게 사용하라정리
#include
구문은 의존성을 최소화 시킬 것