Skip to content

Cpp Coding Standard

JaeYoung Seon edited this page Mar 9, 2021 · 5 revisions

클래스 구성

클래스는 기본적으로 독자의 마음으로 작성해야한다. 대부분의 독자들은 클래스의 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.
 */

명명 규칙

기본적으로 변수, 메서드, 클래스들의 이름은 명확해야하고, 구체적이며, 애매해서는 안된다. 클래스가 널리 쓰이면 쓰일 수록 더 좋고 구체적인 이름을 사용하라. 줄임말은 거르는 것이 좋다.

  1. 클래스와 구조체의 이름은 파스칼 표기법을 따른다

    class PlayerManager;
    struct AnimationInfo;
  2. 지역 변수 그리고 함수의 매개 변수의 이름은 카멜 표기법을 따른다

    void SomeMethod(const int someParameter)
    {
        int someNumber;
        int id;
    }
  3. 메서드 또는 함수 이름은 동사로 시작한다.

    1. public 메서드의 이름은 파스칼 표기법을 따른다

      public:
          void DoSomething();
    2. 그 외 다른 메서드의 이름은 카멜 표기법을 따른다

      private:
          void doSomething();
  4. 절차(반환값이 없는 함수)는 반드시 명확한 의미를 갖는 타동사와 목적어를 사용한다. 다만, 해당 함수가 클래스의 메서드이기 때문에 문맥상 타동사의 목적어가 분명할 때는 예외로 한다

  5. 불리언형을 반환하는 함수의 경우 질문을 하는 형식으로 작성한다

    bool IsVisible();
    bool ShouldClearBuffer();
  6. 함수의 이름은 반드시 반환하는 것이 어떤 의미를 갖는지를 알 수 있도록 지어야한다

    // 체크해서 true면 뭔 뜻인데?
    bool CheckTea(FTea Tea);
    
    // 아~ true면 차가 fresh하단 뜻이군
    bool IsTeaFresh(FTea Tea);
  7. 상수 또는 #define으로 정의된 상수의 이름은 모두 대문자로 하되 밑줄로 각 단어를 분리한다

    	constexpr int SOME_CONSTANT = 1;
  8. 네임스페이스는 모두 소문자로 작성한다

    namespace abc{};
  9. 부울(boolean)형 변수는 앞에 b를 붙인다

    bool bFired;    // 지역 변수와 public 멤버 변수의 경우
    bool mbFired;   // 클래스의 private 멤버 변수의 경우
  10. 인터페이스를 선언할 때는 앞에 I를 붙인다

    class ISomeInterface;
  11. 열거형을 선언할 때는 앞에 e를 붙인다

    enum class eDirection
    {
        North,
        South
    };
  12. 클래스 멤버 변수 명은 앞에 m을 붙인다

    class Employee
    {
    protected:
    	int mDepartmentID;
    private:
    	int mAge;
    };
  13. goto 레이블 명은 모두 대문자로 하되 밑줄로 각 단어를 분리한다

    goto MY_LABEL;
    
    // ...
    
    MY_LABEL:
        std::cout << "Magic! << std::endl;
        return 0;
  14. 값을 반환하는 함수의 이름은 무엇을 반환하는지 알 수 있게 짓는다

  15. 단순히 반복문에 사용되는 변수가 아닌 경우엔 i, e 같은 변수명 대신 index, employee처럼 변수에 저장되는 데이터를 한 눈에 알아볼 수 있는 변수명을 사용한다.

  16. 뒤에 추가적인 단어가 오지 않는 경우 줄임말은 모두 대문자로 표기한다

    int OrderID;
    int HttpCode;
  17. 구조체는 오직 public 멤버 변수만 가질 수 있다. 구조체의 멤버 변수명은 파스칼 표기법을 따르며, 구조체 안에서는 함수는 사용하지 않는다

  18. 재귀 함수는 이름 뒤에 Recursive를 붙인다

    void FibonacciRecursive();
  19. 파일 이름은 대소문자까지 포함해서 반드시 클래스 이름과 일치해야 한다

    class PlayerAnimation;
    
    PlayerAnimation.cpp
    PlayerAnimation.h
  20. 여러 파일이 하나의 클래스를 이룰 때, 파일 이름은 클래스 이름으로 시작하고, 그 뒤에 밑줄과 세부 항목 이름을 붙인다

    class RenderWorld;
    
    RenderWorld_load.cpp
    RenderWorld_demo.cpp
    RenderWorld_portals.cpp
  21. Reverse OOP 패턴을 사용할 때, 플랫폼 전용 클래스는 위 항목과 비슷한 명명 규칙을 사용한다

    class Renderer;
    
    Renderer.h			// 게임에서 호출되는 모든 Renderer 인터페이스
    Renderer.cpp		// 모든 플랫폼 용 Renderer 구현 소스
    Renderer_gl.h		// Renderer가 호출하는 RendererGL 인터페이스
    Renderer_gl.cpp		// RendererGL 구현 소스
  22. 비트 플래그 열거형은 이름 뒤에 Flags를 붙인다

    enum class eVisibilityFlags
    {
    }
  23. 변수 가리기(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
            }
        }
    }

가이드라인

  1. 클래스 멤버 변수에 접근할 때는 항상 setter와 getter를 사용한다

    • 틀린 방식:

      class Employee
      {
      public:
      	string Name;
      };
    • 올바른 방식:

      class Employee
      {
      public:
      	const string& GetName() const;
      	void SetName(const string& name);
      private:
      	string mName;
      };
  2. 외부 헤더 파일을 인클루드 할 때는 #include <>을 사용, 자체적으로 만든 헤더 파일을 인클루드 할 때는 #include ""를 사용한다.

  3. 외부 헤더 파일을 먼저 인클루드한 뒤, 내부 헤더 파일을 인클루드를 할 때, 가능하다면 알파벳 순서를 따른다

  4. 모든 헤더 파일 첫 번째 줄에 #pragma once를 기재한다

  5. 지역 변수를 선언할 때는 그 지역 변수를 사용하는 코드와 동일한 줄(스코프)에 선언하는 것을 원칙으로 한다

  6. double이 반드시 필요한 경우가 아닌 이상 부동 소수점 값에 f를 붙여준다

    float f = 0.5f;
  7. switch case 문에는 항상 default를 넣는다

  8. switch case 문 끝에 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;
      }
  9. default case가 절대 실행될 일이 없는 경우, default case 안에 Assert(false);란 코드를 추가한다. Assert()는 직접 구현하면 그 안에서 릴리즈 빌드 시 최적화 힌트를 추가할 수 있다

    switch (type)
    {
    case 1:
        ... 
        break;
    default:
        Assert(false, "unknown type");
        break;
    }
  10. 원칙적으로 모든 곳에 const를 사용한다. 여기에는 지역 변수와 함수 매개 변수도 포함한다

  11. 개체를 수정하지 않는 멤버 함수에는 모두 const를 붙인다

    int GetAge() const;
  12. 값(value) 형식의 변수를 const로 반환하지 않는다. 포인터나 참조(reference)를 반환할 경우에만 const 반환을 한다

  13. 클래스 안에서 멤버 변수와 메서드의 등장 순서는 다음을 따른다

    1. friend 클래스들
    2. public 메서드들
    3. protected 메서드들
    4. private 메서드들
    5. protected 변수들
    6. private 변수들
  14. 대부분의 경우 함수 오버로딩을 피한다

    • 틀린 방식

      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;
  15. const 반환을 위한 함수 오버로딩은 허용한다

    Anim* GetAnimByIndex(const int index);
    const Anim* GetAnimByIndex(const int index) const;
  16. const_cast를 직접적으로 사용하지 않는다. 대신 const인 개제츨 수정 가능한 형태로 변환해서 반환하는 함수를 만든다

  17. 클래스는 각각 독립된 소스 파일에 있어야 한다. 단, 작은 클래스 몇 개를 한 파일에 같이 넣어 두는 것이 상식적일 경우 예외를 허용한다

  18. 표준 C assert 대신에 자신만의 Assert 버전을 구현한다

  19. 특정 조건이 반드시 충족되어야 한다고 가정(assertion)하고 짠 코드 모든 곳에 assert를 사용한다. Assert는 복구 불가능한 조건이다. Assert는 릴리즈 빌드에서 [__assume](https://docs.microsoft.com/en-us/cpp/intrinsics/assume?view=msvc-160) 키워드로 대체하여 컴파일러에 최적화 힌트를 줄 수 있다

  20. 모든 메모리 할당은 직접 구현한 New, Delete 키워드를 통해 호출한다

  21. memeset, memcpy, memmove와 같은 메모리 연산 역시 우리 고유의 MemSet, MemCpy, MemMove 키워드를 통해 호출해야 한다

  22. 어떤 이유로든 매개변수로 nullptr가 넘어올 수 있는 경우가 아니라면 포인터 대신 참조자(&)를 사용하는 것을 원칙으로 한다 (예외는 다음 항목을 참고)

  23. 함수에서 매개변수를 통해 값을 반환할 때(out 매개변수)는 포인터를 사용하며, 매개변수 이름 앞에 out을 붙인다

    • 함수

      void GetScreenDimension(uint32_t* const outWidth, uint32_t* const  outHeight)
      {
      }
    • 호출

      uint32_t width;
      uint32_t height;
      GetScreenDimension(&width, &height);
  24. 위 항목의 out 매개변수는 반드시 null이 아니어야 한다 (함수 내부에서 if문 대신 assert를 사용할 것)

    void GetScreenDimension(uint32_t* const outWidth, uint32_t* const  outHeight)
    {
        Assert(outWidth);
        Assert(outHeight);
    }
  25. 매개변수가 클래스 내부에서 저장될 때는 포인터를 사용한다

    void AddMesh(Mesh* const mesh)
    {
        mMeshCollection.push_back(mesh);
    }
  26. 매개변수가 void 포인터야 하는 경우는 포인터를 사용한다

    void Update(void* const something)
    {
    }
  27. 특정 크기(예를 들어 데이터 멤버의 직렬화를 위한 크기)가 필요하지 않은 한 열거형에 크기 지정자를 추가하지 않는다

    enum class eDirection : uint8_t
    {
        North,
        South
    }
  28. 디폴트 매개 변수 대신 함수 오버로딩을 선호한다

  29. 디폴트 매개 변수를 사용하는 경우, nullptrfalse, 0 같이 비트 패턴이 0인 값을 사용한다

  30. 가능한 고정된 크기(size)의 컨테이너를 사용한다

  31. 동적 컨테이너를 사용해야 한다면 가능한 한 미리 `reserve()``를 호출한다.

  32. #define으로 정의된 상수는 항상 괄호로 감싸준다

    #define NUM_CLASSES (1)
  33. 상수는 #define보다 const 상수 변수로 선언한다

  34. 클래스를 상호 참조할 때는 #include보다 전방선언(forward declaration)을 최대한 이용한다

  35. 모든 컴파일러 경고는 반드시 고친다

  36. 한 줄에 변수 하나만 선언한다

    • 틀린 방식

      int counter = 0, index = 0;
    • 올바른 방식

      int counter = 0;
      int index = 0;
    • 이래야 각 변수에 대해 주석을 남겨 이 변수가 어떤 의미인지를 알려줄 수 있기 때문이다

  37. 지역 객체를 반환할 때 NRVO의 이점을 활용한다. 이는 함수 내에 하나의 return문 만 쓴다는 것을 의미하며, 이것은 값으로 객체를 반환할 때만 적용된다.

  38. structclass에서 초기화 후 값 변경을 막으려고 const 멤버 변수를 쓰지 않는다. 참조(&) 멤버변수의 경우도 마찬가지

  39. 멤버 변수를 초기화할 때는 초기화 리스트를 사용하는 것을 기본으로 한다

모던 C++ 가이드라인

  1. 컴파일 도중 assertion이 필요하다면 static_assert를 사용한다

  2. overridefinal 키워드를 반드시 사용한다

  3. 항상 enum class를 사용한다

    enum class eDirection
     {
         North,
         South
     }
  4. 가능한 Assert 대신 static_assert 를 사용한다.

  5. 포인터에 NULL 대신 nullptr 를 사용합니다.

  6. 개체의 수명이 클래스 내에서만 처리되는 경우 unique_ptr 를 사용한다. (즉, 개체 생성은 생성자에서, 개체 파괴는 소멸자에서)

  7. 적용 가능한 곳이라면 범위기반 for 문을 사용한다.

  8. 반복자나 new 키워드가 같은 줄에 있어서, 어떤 개체가 만들어지는 지 명확하게 드러나는 경우가 아니라면 auto 키워드를 사용하지 않는다.

  9. std::move를 사용하여 수동으로 반환 값을 최적화하지 않는다. 이럴 경우, 자동 NRVO 최적화가 적용되지 않는다.

  10. 이동 생성자(move constructor)와 이동 대입 연산자(move assignment operator)를 사용해도 된다.

  11. 단순 상수 변수에는 const 대신 constexpr 을 사용한다.

    적용 전:

    const int DEFAULT_BUFFER_SIZE = 65536;
    
    

    적용 후:

    constexpr int DEFAULT_BUFFER_SIZE = 65536;
    
  12. 람다 함수의 경우 sort 등의 함수에 매개변수로 전달하는 등이 아니라면 지양하는 것이 좋으며, 사용하더라도 길이가 과도하게 길어서는 안된다

코드 포맷팅

  1. include 전처리문 블록과 코드 본문 사이에 반드시 빈 줄이 있어야 한다

  2. 탭(tab)은 비주얼 스튜디오 기본값을 사용하며, 비주얼 스튜디오를 사용하지 않을 시 띄어쓰기 4칸을 탭으로 사용한다.

  3. 중괄호( { )를 열 때는 언제나 새로운 줄에 연다.

  4. 중괄호 안( { } )에 코드가 한 줄만 있더라도 반드시 중괄호를 사용한다.

     if (bSomething)
     {
         return;
     }
  5. if - else 블록을 사용할 경우 elseif의 중괄호 다음 줄에서부터 작성한다

    if (bHaveUnrealLicense)
    {
        InsertYourGameHere();
    }
    else
    {
        CallMarkRein();
    }
  6. 포인터나 참조 기호는 자료형에 붙인다.

     int& number;
     int* number;
  7. 초기화 리스트를 이용해 멤버 변수를 초기화할 때는 아래와 같은 포맷을 따라 한 줄에 변수 하나씩 초기화한다.

    • 틀린 방식:
     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)
     {

비주얼 스튜디오 관련

  1. Visual C++: 프로젝트 설정을 변경하려면 항상 속성 시트(property sheets)에서 변경 한다.
  2. 프로젝트 설정에서 컴파일 경고를 비활성화 하지 않는다. 그 대신, 코드에서 #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 바이트).

주석 가이드라인

  1. 애초에 코드 자체가 document가 될 수 있게 작성하라

    // Bad:
    t = s + l - b;
    
    // Good:
    TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves;
  2. 의미있는 주석을 작성하자

    // Bad:
    // increment Leaves
    ++Leaves;
    
    // Good:
    // we know there is another tea leaf
    ++Leaves;
  3. 코드를 제대로 못 짠 거에 주석을 넣어주지 말라. 차라리 다시 코드를 짜라.

    // 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;
  4. 코드와 내용이 상반되어서는 안된다

    // Bad:
    // never increment Leaves!
    ++Leaves;
    
    // Good:
    // we know there is another tea leaf
    ++Leaves;