-
Notifications
You must be signed in to change notification settings - Fork 6
Cpp Coding Standard
클래스는 기본적으로 독자의 마음으로 작성해야한다. 대부분의 독자들은 클래스의 public한 부분을 사용할 것이므로 public이 먼저 나오고 나서 protected, private 순으로 나와야한다.
모든 소스파일(.h, .cpp 등)은 반드시 첫번째 줄에 저작권 표시를 해야한다:
/*!
* Copyright (c) 2021 SWTube. All rights reserved.
* Licensed under the GPL-3.0 License. See LICENSE file in the project root for license information.
*/기본적으로 변수, 메서드, 클래스들의 이름은 명확해야하고, 구체적이며, 애매해서는 안된다. 클래스가 널리 쓰이면 쓰일 수록 더 좋고 구체적인 이름을 사용하라. 줄임말은 거르는 것이 좋다.
-
클래스와 구조체의 이름은 파스칼 표기법을 따른다
class PlayerManager; struct AnimationInfo;
-
지역 변수 그리고 함수의 매개 변수의 이름은 카멜 표기법을 따른다
void SomeMethod(const int someParameter) { int someNumber; int id; }
-
메서드 또는 함수 이름은 동사로 시작한다.
-
public메서드의 이름은 파스칼 표기법을 따른다public: void DoSomething();
-
그 외 다른 메서드의 이름은 카멜 표기법을 따른다
private: void doSomething();
-
-
절차(반환값이 없는 함수)는 반드시 명확한 의미를 갖는 타동사와 목적어를 사용한다. 다만, 해당 함수가 클래스의 메서드이기 때문에 문맥상 타동사의 목적어가 분명할 때는 예외로 한다
-
불리언형을 반환하는 함수의 경우 질문을 하는 형식으로 작성한다
bool IsVisible(); bool ShouldClearBuffer();
-
함수의 이름은 반드시 반환하는 것이 어떤 의미를 갖는지를 알 수 있도록 지어야한다
// 체크해서 true면 뭔 뜻인데? bool CheckTea(FTea Tea); // 아~ true면 차가 fresh하단 뜻이군 bool IsTeaFresh(FTea Tea);
-
상수 또는
#define으로 정의된 상수의 이름은 모두 대문자로 하되 밑줄로 각 단어를 분리한다constexpr int SOME_CONSTANT = 1;
-
네임스페이스는 모두 소문자로 작성한다
namespace abc{};
-
부울(boolean)형 변수는 앞에
b를 붙인다bool bFired; // 지역 변수와 public 멤버 변수의 경우 bool mbFired; // 클래스의 private 멤버 변수의 경우
-
인터페이스를 선언할 때는 앞에
I를 붙인다class ISomeInterface;
-
열거형을 선언할 때는 앞에
e를 붙인다enum class eDirection { North, South };
-
클래스 멤버 변수 명은 앞에
m을 붙인다class Employee { protected: int mDepartmentID; private: int mAge; };
-
goto레이블 명은 모두 대문자로 하되 밑줄로 각 단어를 분리한다goto MY_LABEL; // ... MY_LABEL: std::cout << "Magic! << std::endl; return 0;
-
값을 반환하는 함수의 이름은 무엇을 반환하는지 알 수 있게 짓는다
-
단순히 반복문에 사용되는 변수가 아닌 경우엔
i,e같은 변수명 대신index,employee처럼 변수에 저장되는 데이터를 한 눈에 알아볼 수 있는 변수명을 사용한다. -
뒤에 추가적인 단어가 오지 않는 경우 줄임말은 모두 대문자로 표기한다
int OrderID; int HttpCode;
-
구조체는 오직
public멤버 변수만 가질 수 있다. 구조체의 멤버 변수명은 파스칼 표기법을 따르며, 구조체 안에서는 함수는 사용하지 않는다 -
재귀 함수는 이름 뒤에
Recursive를 붙인다void FibonacciRecursive();
-
파일 이름은 대소문자까지 포함해서 반드시 클래스 이름과 일치해야 한다
class PlayerAnimation; PlayerAnimation.cpp PlayerAnimation.h
-
여러 파일이 하나의 클래스를 이룰 때, 파일 이름은 클래스 이름으로 시작하고, 그 뒤에 밑줄과 세부 항목 이름을 붙인다
class RenderWorld; RenderWorld_load.cpp RenderWorld_demo.cpp RenderWorld_portals.cpp
-
Reverse OOP 패턴을 사용할 때, 플랫폼 전용 클래스는 위 항목과 비슷한 명명 규칙을 사용한다
class Renderer; Renderer.h // 게임에서 호출되는 모든 Renderer 인터페이스 Renderer.cpp // 모든 플랫폼 용 Renderer 구현 소스 Renderer_gl.h // Renderer가 호출하는 RendererGL 인터페이스 Renderer_gl.cpp // RendererGL 구현 소스
-
비트 플래그 열거형은 이름 뒤에
Flags를 붙인다enum class eVisibilityFlags { }
-
변수 가리기(variable shadowing)은 허용되지 않는다. 외부 변수가 동일한 이름을 사용 중이라면 내부 변수에는 다른 이름을 사용한다
class SomeClass { public: int32_t Count; public: void Func(const int32_t Count) { for (int32_t count = 0; count != 10; ++count) { // Use Count } } }
-
클래스 멤버 변수에 접근할 때는 항상 setter와 getter를 사용한다
-
틀린 방식:
class Employee { public: string Name; };
-
올바른 방식:
class Employee { public: const string& GetName() const; void SetName(const string& name); private: string mName; };
-
-
외부 헤더 파일을 인클루드 할 때는
#include <>을 사용, 자체적으로 만든 헤더 파일을 인클루드 할 때는#include ""를 사용한다. -
외부 헤더 파일을 먼저 인클루드한 뒤, 내부 헤더 파일을 인클루드를 할 때, 가능하다면 알파벳 순서를 따른다
-
모든 헤더 파일 첫 번째 줄에
#pragma once를 기재한다 -
지역 변수를 선언할 때는 그 지역 변수를 사용하는 코드와 동일한 줄(스코프)에 선언하는 것을 원칙으로 한다
-
double이 반드시 필요한 경우가 아닌 이상 부동 소수점 값에f를 붙여준다float f = 0.5f;
-
switchcase문에는 항상default를 넣는다 -
switchcase문 끝에break;를 넣지 않고 그 바로 아래case문의 코드를 실행하고 싶은 경우, 미리 정의해둔FALLTHROUGH매크로를 추가한다. 단,case문 안에 코드가 없는 경우는 예외이다. 이는 C++17 사양에서[[fallthrough]]애트리뷰트로 대체될 것이다.-
C++17 이전
switch (number) { case 0: DoSomething(); FALLTHROUGH case 1: DoFallthrough(); break; case 2: case 3: DoNotFallthrough(); break; default: break; }
-
C++17 이후
switch (number) { case 0: DoSomething(); [[fallthrough]]; case 1: DoFallthrough(); break; case 2: case 3: DoNotFallthrough(); break; default: break; }
-
-
defaultcase가 절대 실행될 일이 없는 경우,defaultcase안에Assert(false);란 코드를 추가한다.Assert()는 직접 구현하면 그 안에서 릴리즈 빌드 시 최적화 힌트를 추가할 수 있다switch (type) { case 1: ... break; default: Assert(false, "unknown type"); break; }
-
원칙적으로 모든 곳에
const를 사용한다. 여기에는 지역 변수와 함수 매개 변수도 포함한다 -
개체를 수정하지 않는 멤버 함수에는 모두
const를 붙인다int GetAge() const;
-
값(value) 형식의 변수를
const로 반환하지 않는다. 포인터나 참조(reference)를 반환할 경우에만const반환을 한다 -
클래스 안에서 멤버 변수와 메서드의 등장 순서는 다음을 따른다
-
friend클래스들 -
public메서드들 -
protected메서드들 -
private메서드들 -
protected변수들 -
private변수들
-
-
대부분의 경우 함수 오버로딩을 피한다
-
틀린 방식
const Anim* GetAnim(const int index) const; const Anim* GetAnim(const char* name) const;
-
올바른 방식
const Anim* GetAnimByIndex(const int index) const; const Anim* GetAnimByName(const char* name) const;
-
-
const반환을 위한 함수 오버로딩은 허용한다Anim* GetAnimByIndex(const int index); const Anim* GetAnimByIndex(const int index) const;
-
const_cast를 직접적으로 사용하지 않는다. 대신const인 개제츨 수정 가능한 형태로 변환해서 반환하는 함수를 만든다 -
클래스는 각각 독립된 소스 파일에 있어야 한다. 단, 작은 클래스 몇 개를 한 파일에 같이 넣어 두는 것이 상식적일 경우 예외를 허용한다
-
표준 C
assert대신에 자신만의Assert버전을 구현한다 -
특정 조건이 반드시 충족되어야 한다고 가정(assertion)하고 짠 코드 모든 곳에
assert를 사용한다.Assert는 복구 불가능한 조건이다.Assert는 릴리즈 빌드에서[__assume](https://docs.microsoft.com/en-us/cpp/intrinsics/assume?view=msvc-160)키워드로 대체하여 컴파일러에 최적화 힌트를 줄 수 있다 -
모든 메모리 할당은 직접 구현한
New,Delete키워드를 통해 호출한다 -
memeset,memcpy,memmove와 같은 메모리 연산 역시 우리 고유의MemSet,MemCpy,MemMove키워드를 통해 호출해야 한다 -
어떤 이유로든 매개변수로
nullptr가 넘어올 수 있는 경우가 아니라면 포인터 대신 참조자(&)를 사용하는 것을 원칙으로 한다 (예외는 다음 항목을 참고) -
함수에서 매개변수를 통해 값을 반환할 때(
out매개변수)는 포인터를 사용하며, 매개변수 이름 앞에out을 붙인다-
함수
void GetScreenDimension(uint32_t* const outWidth, uint32_t* const outHeight) { }
-
호출
uint32_t width; uint32_t height; GetScreenDimension(&width, &height);
-
-
위 항목의
out매개변수는 반드시null이 아니어야 한다 (함수 내부에서if문 대신assert를 사용할 것)void GetScreenDimension(uint32_t* const outWidth, uint32_t* const outHeight) { Assert(outWidth); Assert(outHeight); }
-
매개변수가 클래스 내부에서 저장될 때는 포인터를 사용한다
void AddMesh(Mesh* const mesh) { mMeshCollection.push_back(mesh); }
-
매개변수가
void포인터야 하는 경우는 포인터를 사용한다void Update(void* const something) { }
-
특정 크기(예를 들어 데이터 멤버의 직렬화를 위한 크기)가 필요하지 않은 한 열거형에 크기 지정자를 추가하지 않는다
enum class eDirection : uint8_t { North, South }
-
디폴트 매개 변수 대신 함수 오버로딩을 선호한다
-
디폴트 매개 변수를 사용하는 경우,
nullptr나false,0같이 비트 패턴이 0인 값을 사용한다 -
가능한 고정된 크기(size)의 컨테이너를 사용한다
-
동적 컨테이너를 사용해야 한다면 가능한 한 미리 `reserve()``를 호출한다.
-
#define으로 정의된 상수는 항상 괄호로 감싸준다#define NUM_CLASSES (1)
-
상수는
#define보다const상수 변수로 선언한다 -
클래스를 상호 참조할 때는
#include보다 전방선언(forward declaration)을 최대한 이용한다 -
모든 컴파일러 경고는 반드시 고친다
-
한 줄에 변수 하나만 선언한다
-
틀린 방식
int counter = 0, index = 0;
-
올바른 방식
int counter = 0; int index = 0;
-
이래야 각 변수에 대해 주석을 남겨 이 변수가 어떤 의미인지를 알려줄 수 있기 때문이다
-
-
지역 객체를 반환할 때 NRVO의 이점을 활용한다. 이는 함수 내에 하나의 return문 만 쓴다는 것을 의미하며, 이것은 값으로 객체를 반환할 때만 적용된다.
-
struct나class에서 초기화 후 값 변경을 막으려고const멤버 변수를 쓰지 않는다. 참조(&) 멤버변수의 경우도 마찬가지 -
멤버 변수를 초기화할 때는 초기화 리스트를 사용하는 것을 기본으로 한다
-
컴파일 도중 assertion이 필요하다면
static_assert를 사용한다 -
override와final키워드를 반드시 사용한다 -
항상
enum class를 사용한다enum class eDirection { North, South }
-
가능한
Assert대신static_assert를 사용한다. -
포인터에
NULL대신nullptr를 사용합니다. -
개체의 수명이 클래스 내에서만 처리되는 경우
unique_ptr를 사용한다. (즉, 개체 생성은 생성자에서, 개체 파괴는 소멸자에서) -
적용 가능한 곳이라면 범위기반
for문을 사용한다. -
반복자나
new키워드가 같은 줄에 있어서, 어떤 개체가 만들어지는 지 명확하게 드러나는 경우가 아니라면auto키워드를 사용하지 않는다. -
std::move를 사용하여 수동으로 반환 값을 최적화하지 않는다. 이럴 경우, 자동 NRVO 최적화가 적용되지 않는다. -
이동 생성자(move constructor)와 이동 대입 연산자(move assignment operator)를 사용해도 된다.
-
단순 상수 변수에는
const대신constexpr을 사용한다.적용 전:
const int DEFAULT_BUFFER_SIZE = 65536;적용 후:
constexpr int DEFAULT_BUFFER_SIZE = 65536; -
람다 함수의 경우
sort등의 함수에 매개변수로 전달하는 등이 아니라면 지양하는 것이 좋으며, 사용하더라도 길이가 과도하게 길어서는 안된다
-
include전처리문 블록과 코드 본문 사이에 반드시 빈 줄이 있어야 한다 -
탭(tab)은 비주얼 스튜디오 기본값을 사용하며, 비주얼 스튜디오를 사용하지 않을 시 띄어쓰기 4칸을 탭으로 사용한다.
-
중괄호(
{)를 열 때는 언제나 새로운 줄에 연다. -
중괄호 안(
{ })에 코드가 한 줄만 있더라도 반드시 중괄호를 사용한다.if (bSomething) { return; }
-
if-else블록을 사용할 경우else는if의 중괄호 다음 줄에서부터 작성한다if (bHaveUnrealLicense) { InsertYourGameHere(); } else { CallMarkRein(); }
-
포인터나 참조 기호는 자료형에 붙인다.
int& number; int* number;
-
초기화 리스트를 이용해 멤버 변수를 초기화할 때는 아래와 같은 포맷을 따라 한 줄에 변수 하나씩 초기화한다.
- 틀린 방식:
MyClass::MyClass(const int var1, const int var2) :mVar1(var1), mVar2(var2), mVar3(0) {
- 올바른 방식:
MyClass::MyClass(const int var1, const int var2) : mVar1(var1) , mVar2(var2) , mVar3(0) {
- Visual C++: 프로젝트 설정을 변경하려면 항상 속성 시트(property sheets)에서 변경 한다.
- 프로젝트 설정에서 컴파일 경고를 비활성화 하지 않는다. 그 대신, 코드에서
#pragma를 사용한다.
<stdint>에 정의된 C++11 이후 추가된 형들을 사용한다
-
bool: 불리언형 (bool의 크기를 절대로 추정해서는 안된다). - TODO(캐릭터 관련 추가 필요)
-
uint8_t: 무부호 바이트 (1 바이트). -
int8_t: 유부호 바이트 (1 바이트). -
uint16_t: 무부호 "단정수short" (2 바이트). -
int16_t: 유부호 "단정수short" (2 바이트). -
uint23_t: 무부호 정수 (4 바이트). -
int32_t: 유부호 정수 (4 바이트). -
uint64_t: 무부호 "4배 단어quad word" (8 바이트). (Microsoft의 Window.h에WORD라는 16비트 무부호 정수의 4배 크기를 갖음을 의미) -
int64_t: 유부호 "4배 단어quad words" (8 바이트). -
float: 단정밀도 고정 소수점 (4 바이트). -
double: 2배정밀도 고정 소수점 (8 바이트).
-
애초에 코드 자체가 document가 될 수 있게 작성하라
// Bad: t = s + l - b; // Good: TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves;
-
의미있는 주석을 작성하자
// Bad: // increment Leaves ++Leaves; // Good: // we know there is another tea leaf ++Leaves;
-
코드를 제대로 못 짠 거에 주석을 넣어주지 말라. 차라리 다시 코드를 짜라.
// Bad: // total number of leaves is sum of // small and large leaves less the // number of leaves that are both t = s + l - b; // Good: TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves;
-
코드와 내용이 상반되어서는 안된다
// Bad: // never increment Leaves! ++Leaves; // Good: // we know there is another tea leaf ++Leaves;