언리얼에서 개발을 진행중이다보니 언리얼 코딩표준을 따라야한다.
일찍이 정리해두고 알아뒀어야하는데 이제 정리하게 된다. 시니어개발자님이 문서를 알려주셨다 ㅎㅎ
나는 쓰면서 알아가는 공부가 잘 되니 내용이 거의 고대로 있더라도 적어내려가보겠다.
작명규칙
- 첫 글자는 대문자, 단어사이 공백 없이
- 유형이름은 변수 이름과 구분을 위해 접두사를 붙임
- UObject 상속은 U로 시작
- AActor 상속은 A로 시작
- SWidget 상속은 S로 시작
- 추상 인터페이스는 I로 시작
- Enum 은 E
- Boolean 은 b
- 그 외 클래스는 대부분 F로 시작
- Typedef 시에는 적당한 유형을 붙이자
- 유형과 변수명은 명사
- 메서드 이름은 동사로 설명(하는 일 또는 반환값)
- CheckValidity, GetMyScore
- 명확하고 애매하지 않게! 과도한 축약을 피하자
- 변수는 한 줄에 하나만.// 설명하기 좋도록
- bool 반환하는 함수의 경우, true/false 질문을 한다.
- IsVisible() SouldClearBuffer()
- 프로시져(반환값이 없는 함수)는 강한 동사 뒤에 오브젝트를 붙여쓴다.
- 파라미터
- 출력할 것으로 기대되는 경우 Out 접두사 붙임
- 입력으로 기대되는 경우 In 접두사 붙임
- Boolean 인데 In/Out 이라면 bOutResult
- 반환값
- 반환값이 있는 경우, 반환값에 대한 설명이 필요함
float TeaWeight;
int32 TeaCount;
bool bDoesTeaStink;
FName TeaName;
FString TeaFriendlyName;
UClass* TeaClass;
USoundCue* TeaSound;
UTexture* TeaTexture;
코멘트
- 가장 중요한 것은 코멘트가 없어도 설명이 되는 코드 작성하기
- 변수명, 함수명 잘 작성하라는 뜻
- 축약해서 코드를 작성하고 코멘트를 상세하게 작성하지 말라는 뜻
- 설명을 위한 코멘트를 작성하자. 보이는 그대로를 알려주는 코멘트를 작성은 할 필요없음
- 그렇다고 모순되는 코드를 작성하고 코멘트를 달 생각은 말자. 코드를 모순되게 짜면 안됨
Const 정확도를 맞추자
- 함수 인수가 수정되지않는 경우 -> 함수 인자에 const
- 함수가 오브젝트 수정하지않는 경우 -> 함수 자체에 const
- 루프에서 컨테이너에 대한 수정하지 않아 const 사용해서 반복처리 -> for(const Fstring ...)
- 포인터에 const: T* const ptr;
- 포인터가 아닌 자체에 const: const T* ptr;
- 반환형에 const 를 붙이지 마라
- const 배열로의 레퍼런스, const 배열로의 포인터는 괜찮음
- const 배열, const 배열로의 const 반환은 안됨
코맨트 포맷
- JavaDoc 기반 시스템 따라 문서 제작하는 Unreal Engine(Doxygen이 아닐까?)
- 포맷에 따라 주석 작성하면 문서화에 도움이 됨
- 간단한 설명, param, return 등을 적는다.
- // Todo: Doxygen과 관련된 글 쓰기
최신 C++ 언어 문법
C++14 언어 기능을 활용
지원되는 C++컴파일러 기능이 존재하니, 그 외에는 사용을 삼가/조심 해야한다.
static_assert
: 컴파일 시간 assert 가 필요한 경우 사용.
- assert 는 프로그램 수행 중 유지되어야 하는 부분을 검증할 때 사용된다.(절대 음수면 안되는 경우)
- 그 중 static_assert는 컴파일 타임에서의 검증을 위해 사용된다.
template <class T>
void my_assert(T& a)
{
static_assert(std::is_copy_constructible<T>::value,
"Copy constructure is unavailable");
}
override 및 final
: 사용을 권장하는 키워드!
- override 는 하위 클래스에서 override한 가상함수에 붙인다. 이것은 오버라이딩한 가상함수다! 알려주는 것
- fianl 은 더이상 가상함수의 override를 허용하지 않겠다는 뜻. 그 함수를 재정의하는 마지막 하위 클래스에서 사용!
nullptr
: 모든 경우 사용!
- NULL 매크로 대신 nullptr을 사용해야한다.
- 한 가지 예외: C++/CX 빌드의 nullptr은 managed null 레퍼런스 유형
- 그 외에는 거의 호환되긴 하지만 호환성을 위해서는 더 일반적인 decltype(nulltype) 대신 TYPE_OF_NULLPTR 사용!
auto 키워드
: 몇 가지 예외를 제외하고 C++에서 auto는.. 안된다. 초기화 유형은 명시!
- 괜찮은 경우
- 변수에 람다를 바인딩 해아 하는 경우
- 이터레이터 변수의 경우(가독성을 위해)
- 템플릿 코드에서 표현식 유형 쉽게 식별하기 위해
- 반드시, 읽는 사람이 무슨 유형인지 명확하게 알 수 있어야한다!
- auto 를 쓰더라도 const, &, * 를 정확하게 사용해야한다.
범위 기반 for
: 유지보수 향상에 도움되므로 추천!
for(TPair<FString, int32>& Kvp: MyMap)
{
// do something
}
람다 및 무명 함수
: 자유롭게 사용 가능!
- 범용 알고리즘의 술부에서 많이 사용됨.
- 람다의 문서화는 일반 함수와 같은 방식으로 문서화 필요
- 자동캡처보다 수동캡처가 좋음. 퍼포먼스를 위해
- 자동캡처는 묵시적으로 this를 맵처
- 반환형을 명시해주는 것이 좋음
Enum
: 구식 네임스페이스 enum 을 대체하여 사용해야 함
UENUM()
enum class ECocoFood : unit8
{
duck,
lamb
}
UPROPERTY()
ECocoFood CocoFood;
- 플래그로 사용되는 Enum 클래스는 새로운 ENUM_CLASS_FLAGS(EnumType) 매크로 사용 필요
이동 시맨틱
: 최적화를 위해 MoveTemp 사용 가능
- 컨테이너 유형 TArray, TMap, TSet, FString 에는 move생성자와 move 할당 연산자가 있음(lvalue <-> rvalue)
- UE4에서는 MoveTemp를 통해 명시적으로 실행 가능
- 이는 임시 복사하는 비용이 없어 표현상의 이득이 됨
디폴트 멤버 이니셜라이저
: 클래스 자체 내 클래스 기본 값을 정의하는 데 사용 가능
- 클래스의 멤버변수를 선언하는 동시에 초기화 하는 경우를 말함
- 엔진 코드 보다는 게임 코드 쪽에 적합
UPROPERTY()
int32 CocoAge = 13;
- 장점
- 생성자에서 이니셜라이저 복제할 필요 없음
- 초기화 순서 선언순서 섞이지않음
- 가독성 유지보수성에 좋음
- 단점:
- 기본 값 변경 시 종속 파일 리빌드 필요
- 헤더는 엔진 패치 릴리즈에서 변경 불가.. 수정 제한될 수 있음
- 모든것을 초기화할 수 없음. 베이스클래스라던가..
- 여기저기 나눠서 초기화 하면 오히려 가독성 유지보수성이 좋지 않을 수 있음
서드 파티 코드
: 엔진에서 사용하는 라이브러리에 코드를 수정할 때 //@UE4 코멘트로 변경한 이유에 대해 태그 달기
코드 포맷
대괄호
If-Else
- 각 실행 블록은 대괄호로 묶어야함
- 안 묶었다가 의도치않은 실수를 만들 수 있음..
탭 및 들여쓰기
- 실행 블록별로 들여쓴다.
- 줄 시작부분 공백은 스페이스 아닌 탭으로!
Switch 문
- 다음 케이스로 넘어가는 지 명시적으로 밝혀주기! 각 경우마다 break넣기
- break 넣지 않으면, fall through 코멘트 달기 (// falls through)
- default case 는 항상! 그 뒤에 case를 추가할 때를 대비하여 break도 넣는다.
네임스페이스
- 언리얼 코드는 글로벌 네임스페이스에 둘러싸여 있지 않으니, 서드파티 코드 사용 시 전역 범위에서 충돌 일어나지 않게 주의!
- Using 선언
- 전역 범위에서는 안됨. 다른 네임스페이스 안 또는 함수 안에서는 괜찮음
- 언리얼 헤더 툴에는 네임스페이스가 지원되지 않음 -> UCLASS, USTRUCT 등 정의 시에는 사용 불가
물리적 종속성
- 파일 이름은 접두사 붙이지 말기 Scene.cpp
- 모든 헤더는 #pragma once 디렉티브 (지시자)로 복수의 include 방지
- include 시에는 전부 include 보다는 특정 부분을 inlcude 하는것이 좋음.
- 모듈에는 Private 와 Public 소스 디렉터리가 있음.
- 다른 모듈이 필요로 하는 정의는 Public
- 인라인 함수 너무 많이 사용하지 말자. 사용하지 않는 파일도 강제로 리빌드 시킴. 프로파일링 이득이 있는 경우에만 쓰자.
- FORCEINLINE 사용에 있어서는 보수적이어야 한다. // Todo: ForceInline 조사
- 모든 코드와 로컬 변수는 호출하는 함수로 확장 -> 큰 함수에서 발생하는 것과 동일한 빌드 시간 문제 발생
캡슐화
- 보호 키워드로 캡슐화
- 맴버는 public/protected 인터페이스 일부아니면 private!
- 더이상 파생을 원하지 않으면 final 사용
일반적인 스타일 문제
- 종속성은 최소화
- 메서드는 가급적 하위 메서드로 분할. 이름 잘 지어둔 다수의 하위 메서드를 연속적으로 호출하는것이 더 이해하기 쉬움
- 함수 선언이나 함수 호출 위치에서 함수 이름과 인수 목록 선행되는 괄호 사이 공백 금지
- 파일 끝에 빈 줄 하나
- 스트링 리터럴 주변에는 항상 TEXT 매크로 사용!- // Todo: TEXT 매크로 설명
- 루프에서 동일 적업 반복 피하기
- 핫 리로드 기능 염두. 리로드동안 그대로 남아있을 것에만 statics 사용 // Todo: 핫 리로드 조사
- 복잡한 표현식은 중간변수를 사용하여 단순화! (바로 bool 리턴하는 함수를 표현식에 넣는 것이 아니라 변수에 저장한 후 표현식에 넣기)
- 포인터와 레퍼런스 공백은 딱 한칸만 FShaderType Ptr (O)/ FShaderType Ptr (X)
- 변수 음영(shadowed) 허용안됨(멤버변수와 같은 이름인 변수를 사용하는 것)
- 함수 호출에서 익명 리터럴 피하기
- 리터럴이란, 정해진 값을 말하는 것
- 함수호출에 값을 바로 넣는지 않아야한다는 것(FeedCoco(1, TEXT("코코야 밥먹자"))안된다는 것)
API 디자인 지침
- bool 함수 파라미터가 많아지면, enum을 사용하자
- 너무 긴 함수 파라미터는 struct 사용을 고려해보자
- bool 및 FString 사용한 함수 오버로드는 피하자
- I 접두사를 가진 클래스는 항상 추상형이어야한다. 순수가상함수만 있어야하며, 멤버변수 있음 안됨.
- 오버라이딩 메서드 선언시에는 virtual, override 키워드 사용 (virtual void F() override;)
플랫폼별 코드
- 플랫폼별 코드는 적합한 이름의 하위 디렉터리 아래 플랫폼별 소스 파일에 추상화 및 구현하야함
Source/Runtime/Core/Private/[Platform]/[Platform]Memory.cpp
- 하드웨어 추상 층 확장해서 static 함수 추가