Naming

명명(naming) 및 주석(comment)을 포함한 모든 코드에서 포괄적인 언어를 사용하고 다른 프로그래머가 무례하거나 불쾌하다고 생각할 수 있는 용어는 사용하지 않는다.

  • such as "master" and "slave", "blacklist" and "whitelist", or "redline”

마찬가지로, 특정 사람을 언급하지 않는 한 중립적인 언어를 사용한다.

  • such as "they"/"they"/"their", "it"/"its"

General Naming Rules

가독성을 최우선으로, 이름은 명확해야하고 줄임말(프로젝트 외부에서 알기 어려운 약어)은 피한다.

  • 단어 내부에서 글자를 삭제하여 축약하는 방식은 사용하지 않음 (cstmr_id ❌ , customer_id ✅)
  • 해당 약어가 위키피디아에 있다면 사용해도 괜찮음

짧은 코드 범위에서는 n, i 같은 이름이 괜찮지만, 클래스 범위에서는 더 구체적인 이름을 사용해야 한다.

  • 짧은 코드 = 5줄 함수
  • kMaxAllowedConnections ✅, kNum ❌

대문자 약어는 하나의 단어처럼 사용

  • StartRpc() ✅, StartRPC() ❌

File Names

모두 소문자여야 하며 밑줄(_) 또는 대시(-)를 포함할 수 있다.

  • 프로젝트에서 사용하는 규칙이 우선이며, 규칙이 없다면 밑줄을 선호한다.
my_useful_class.cc
my-useful-class.cc
myusefulclass.cc
myusefulclass_test.cc // _unittest and _regtest are deprecated.

 

파일 확장자 규칙을 따른다.

파일 유형 확장자 설명
소스 파일 .cc C++ 소스 코드 파일
헤더 파일 .h C++ 헤더 파일
포함 전용 파일 .inc 특정 위치에서 포함되는 파일

 

고유하면서 구체적인 파일명을 사용하고 시스템 헤더 파일과 이름 충돌을 피한다. (db.h, string.h)

  • 너무 일반적이지 않고 구체적인 의미를 담아야한다.
  • 좋은 예시 vs 나쁜 예시
logs.h               ❌ (너무 일반적)
http_server_logs.h   ✅ (더 구체적인 의미 전달)
foo_bar.h            ✅ (FooBar 클래스를 정의하는 파일)
foo_bar.cc           ✅ (FooBar 클래스의 구현 파일)

Type Names

항상 대문자로 시작하고, 단어 사이에 _ (언더스코어)를 사용하지 않는다.

새로운 단어의 시작은 대문자로 구분한다(PascalCase)

모든 타입(Class, Struct, Type Alias, Enum, Template parameters)에 동일한 규칙을 적용한다.

// classes and structs
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...

// typedefs
typedef hash_map<UrlTableProperties *, std::string> PropertiesMap;

// using aliases
using PropertiesMap = hash_map<UrlTableProperties *, std::string>;

// enums
enum class UrlTableError { ...

Variable Names

변수 및 함수 매개변수는 snake_case (소문자 + 언더스코어) 형식을 사용한다.

std::string table_name;  // ✅ 올바른 변수명
int max_value;  // ✅ 올바른 변수명
void process_data(int input_value) { ... }  // ✅ 함수 매개변수도 snake_case 사용

std::string tableName;  // ❌ camelCase 사용 금지
std::string TableName;  // ❌ PascalCase 사용 금지
void ProcessData(int InputValue) { ... }  // ❌ 변수명에 대문자 사용 금지

Class의 데이터 멤버는 snake_case_ (언더스코어로 끝나도록) 형식을 사용한다.

class TableInfo {
 private:
  std::string table_name_;  // ✅ 클래스 데이터 멤버는 _로 끝남
  int record_count_;  // ✅ 클래스 내부 변수는 _로 끝남
  static Pool<TableInfo>* pool_;  // ✅ 정적 변수도 동일한 규칙 적용
};

class TableInfo {
 private:
  std::string tableName;  // ❌ camelCase 사용 금지
  int recordCount;  // ❌ camelCase 사용 금지
  static Pool<TableInfo>* Pool;  // ❌ PascalCase 사용 금지
};

Struct 의 데이터 멤버는 일반 변수처럼 snake_case를 사용한다.

struct UrlTableProperties {
  std::string name;  // ✅ 일반 변수처럼 snake_case 사용
  int num_entries;  // ✅ 정수형 변수도 snake_case 사용
  static Pool<UrlTableProperties>* pool;  // ✅ 정적 변수도 동일한 규칙 적용
};

struct UrlTableProperties {
  std::string name_;  // ❌ 구조체에서는 `_`를 붙이지 않음
  int numEntries;  // ❌ camelCase 사용 금지
  static Pool<UrlTableProperties>* Pool_;  // ❌ PascalCase 및 `_` 사용 금지
};

Constant Names

constexpr 또는 const 변수는 k로 시작해야하며, 다음 단어부터는 CamelCase를 적용한다. (kCamelCase)

단어 구분이 어려운 경우에만 _(언더스코어)를 사용한다.

const int kDaysInAWeek = 7;  // ✅ 'k'로 시작, CamelCase 사용
constexpr double kPi = 3.14159;  // ✅ 'k' 사용, 프로그램 실행 중 변하지 않음
const int kAndroid8_0_0 = 24;  // ✅ 언더스코어 사용 (숫자 구분이 어려운 경우)

template <typename T>
constexpr int kMaxBufferSize = 1024;  // ✅ 템플릿 변수지만, 모든 인스턴스에서 상수로 유지됨

지역 변수의 경우에는 적용할 필요가 없다.

void ComputeFoo(absl::string_view suffix) {
  const absl::string_view kPrefix = "prefix";  // ✅ 사용 가능 (지역 변수지만, 프로그램 실행 중 변하지 않음)
  const absl::string_view prefix = "prefix";  // ✅ 일반적인 네이밍도 허용됨
}

void ComputeFoo(absl::string_view suffix) {
  const std::string kCombined = absl::StrCat(kPrefix, suffix);  // ❌ 잘못된 예제

Function Names

일반적인 함수는 PascalCase 를 사용해야 한다.

void AddTableEntry();  // ✅ 올바른 예시
void DeleteUrl();  // ✅ 올바른 예시
void OpenFileOrDie();  // ✅ 올바른 예시

void add_table_entry();  // ❌ 함수는 PascalCase를 사용해야 함
void delete_url();  // ❌ 함수 이름을 소문자로 작성하면 안 됨

단, getter와 setter의 경우에는 변수처럼 snake_case 를 사용한다.

class Counter {
 public:
  int count() const { return count_; }  // ✅ 변수처럼 `snake_case` 스타일 사용 가능
  void set_count(int count) { count_ = count; }  // ✅ setter도 변수 스타일 허용

 private:
  int count_;
};

Namespace Names

소문자로 작성하며, 단어 사이에는 _(언더스코어)를 사용한다.

최상위 네임스페이스는 프로젝트 또는 팀 이름을 반영해야한다.

namespace websearch {  // ✅ 프로젝트 이름을 최상위 네임스페이스로 사용
namespace index_util {  // ✅ 단어 구분 시 언더스코어 사용
void BuildIndex();
}  // namespace index_util
}  // namespace websearch

namespace WebSearch {  // ❌ 대문자로 시작하면 안 됨
namespace IndexUtil {  // ❌ CamelCase 사용 금지
void BuildIndex();
}  // namespace IndexUtil
}  // namespace WebSearch

잘 알려진 최상위 네임스페이스(std, boost 등)와 충돌하지 않도록 해야한다.

네임스페이스 이름을 축약하면 안된다.

namespace websearch {  // ✅ 최상위 네임스페이스 = 프로젝트 이름
namespace index {  // ✅ 서브 네임스페이스 (구체적인 역할을 반영)
void BuildIndex();
}  // namespace index
}  // namespace websearch

namespace util {  // ❌ "util"은 너무 일반적이며 충돌 가능성이 높음
namespace index {
void BuildIndex();
}  // namespace index
}  // namespace util

Enumerator Names

모든 열거형(enum) 값은 kCamelCase 스타일을 사용해야 한다.

enum class UrlTableError {
  kOk = 0,             // ✅ 올바른 네이밍
  kOutOfMemory,        // ✅ CamelCase + k 접두사
  kMalformedInput,     // ✅ CamelCase + k 접두사
};

enum class AlternateUrlTableError {
  OK = 0,              // ❌ 매크로처럼 작성하면 안 됨
  OUT_OF_MEMORY = 1,   
  MALFORMED_INPUT = 2, 
};

Macro Names

매크로는 가급적 사용하지 않는다. 꼭 필요한 경우 SCREAMING_SNAKE_CASE(대문자+ _ )를 사용한다.

매크로 이름에는 프로젝트 접두사(prefix)를 포함한다.

#define MYPROJECT_ROUND(x) ((x) + 0.5)  // ✅ 프로젝트 접두사 포함
#define MYPROJECT_MAX_BUFFER_SIZE 1024  // ✅ 프로젝트 접두사 포함
#define DEBUG_MODE 1  // ✅ 글로벌 매크로는 대문자로 작성

#define MyProjectRound(x) ((x) + 0.5)  // ❌ PascalCase 사용 금지
#define my_project_max_buffer_size 1024  // ❌ 소문자 사용 금지
#define DebugMode 1  // ❌ CamelCase 사용 금지

일반적인 코드에서는 constexpr, inline, const 등을 대신 사용할 것을 권장한다.

  • 예시 1. 매크로 사용의 좋지 않은 방법과 그 대안
#define SQUARE(x) ((x) * (x))  // ❌ 위험: x가 식일 경우 예상치 못한 동작 가능

int result = SQUARE(3 + 1);  // 예상: (3+1) * (3+1) = 16
// 실제 동작: (3 + 1 * 3 + 1) = 7 (잘못된 결과)

// constexpr을 사용하는 방법
constexpr int Square(int x) {
    return x * x;
}

Exceptions to Naming Rules

C 표준 라이브러리와 유사한 함수는 기존 스타일을 유지한다.

int bigopen(const char* filename, int flags); // ✅ 기존 open() 함수 스타일 유지
int BigOpen(const char* filename, int flags); // ❌ C 스타일 함수는 PascalCase 사용하지 않음

C 스타일 타입 정의 (typedef or using)은 기존 스타일을 유지한다.

typedef unsigned int uint;  // ✅ 기존 C 스타일 유지
using uint = unsigned int;  // ✅ C++ 스타일 유지

typedef unsigned int UnsignedInt;  // ❌ `typedef`는 일반적으로 소문자 사용

STL 스타일을 따르는 경우, 기존 스타일을 유지한다.

template <typename Key, typename Value>
class sparse_hash_map {
    // ✅ STL 스타일 유지
};

template <typename Key, typename Value>
class SparseHashMap {  // ❌ STL 스타일을 따르지 않음
};

상수(constexpr 또는 #define)는 기존 C 스타일(SCREAMING_SNAKE_CASE)을 유지해야 한다.

#define LONGLONG_MAX 9223372036854775807LL  // ✅ 기존 INT_MAX 스타일 유지
constexpr long long LONGLONG_MAX = 9223372036854775807LL;  // ✅ 같은 스타일 유지

constexpr long long LongLongMax = 9223372036854775807LL;  // ❌ 기존 C 스타일과 다름

Functions

Inputs and Outputs

함수의 출력은 자연스럽게 반환 값을 통하며, 때로는 매개 변수를 통해 제공한다.

매개변수 보다 반환 값을 사용하는 것을 선호한다.

  • 가독성을 향상시키고 종종 동일하거나 더 나은 성능을 제공
  • Null 이 될 수 없는 한 원시 포인터를 반환하지 않음

std::optional을 사용하여 값에 의한 선택적 입력을 나타내고, 비 선택적일 때 const 포인터를 사용한다.

Write Short Functions

가능한 짧고 단순하게 작성하여 작고 집중된 기능으로 구현한다.

  • 단, 함수를 구성하는 코드 길이에 대한 제한은 없다.
  • 40 줄을 초과하는 경우에는 프로그램의 구조를 해치지 않고 분해할 수 있는지 고민해보길 권유
  • 함수를 짧고 단순하게 유지하면 다른 사람들이 코드를 더 쉽게 읽고 수정할 수 있음
  • 기능 별로 나눠두면 테스트 하기에 용이함

Function Overloading

함수 오버로딩을 사용할 때는 호출하는 코드만 보고도 어떤 함수가 실행 될지 쉽게 알 수 있어야 한다.

  • 예시 1. 함수의 의미가 다른데 적절하지 않게 오버로딩을 사용한 경우
void Process(int value);
void Process(double value);
void Process(bool flag);
void Process(const std::string& str);
void Process(const std::vector<int>& data);

 

const 또는 & (래퍼런스)로 인한 오버로딩은 유용하게 활용이 가능하다.

 

class Widget {
 public:
  void PrintInfo() &;   // 인스턴스에서 호출 가능
  void PrintInfo() &&;  // 임시 객체에서만 호출 가능
};

Widget w;
w.PrintInfo();  // ✅ PrintInfo() & 호출됨
Widget().PrintInfo();  // ✅ PrintInfo() && 호출됨

Default Arguments

가독성을 높이는 데 유용하지만, 오버로딩과 비교하여 신중하게 선택해야한다.

가상(virtual) 함수에서는 사용하지 않는다.

class Base {
public:
    virtual void Speak(std::string msg = "Hello from Base!") {
        std::cout << msg << std::endl;
    }
};

class Derived : public Base {
public:
    void Speak(std::string msg = "Hello from Derived!") override {
        std::cout << msg << std::endl;
    }
};

int main() {
    Derived d;
    Base* b = &d;
    
    b->Speak();  // "Hello from Base!" 출력 (❌ 예상과 다름)
}

Trailing Return Type Syntax

람다(lambda) 함수와 같이 특정 상황에서만 필요한 선언 스타일로, 일반적으로는 선행 반환 타입(Leading Return Type)을 사용한다.

  • 선행 반환 타입과 후행 반환 타입(Trailing Return Type)
int foo(int x); // 선행 반환 타입
auto foo(int x) -> int; // 후행 반환 타입, C++ 11 이상
  • 람다에서 반환 타입을 명시할 때는 반드시 후행 타입을 사용
  • 생략하면 컴파일러가 자동으로 반환 타입을 추론
auto lambda = [](int x, int y) -> int {
    return x + y;
};
  • 템플릿에서 반환 타입이 매개변수에 따라 달라질 때 사용
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u);

Classes

Doing Work in Constructors

생성자에서 가상 메서드를 호출하지 않도록 하고, 오류 신호를 보낼 수 없는 경우 초기화가 실패할 수 있을 것을 방지해야 한다.

Implicit Conversions

암시적 변환을 정의하지말고, explicit 키워드를 사용한다.

class Foo {
  explicit Foo(int x, double y);
  ...
};

void Func(Foo f);
Func({42, 3.14});  // Error
Func(Foo(42, 3.14)); // OK

Copyable and Movable Types

클래스의 public API는 클래스가 복사가 가능한지, 이동만 가능한지, 복사하거나 이동할 수 없는지 여부를 분명하게 해야 한다.

  1. 복사 가능(Copyable)한 객체
  • 같은 타입의 다른 객체를 사용해서 생성 가능 (복사 생성자)
  • 같은 타입의 다른 객체를 사용해서 대입 가능 (복사 대입 생성자)
  • 원본 객체의 값은 변하지 않음 (즉, 원본과 동일한 객체를 가진 새로운 객체가 생성됨)
  • 예시 1. 복사 가능한 객체
int a = 10;
int b = a;  // ✅ 복사 가능

std::string s1 = "hello";
std::string s2 = s1;  // ✅ 복사 가능 (s2도 "hello"를 가짐)
  • 예시 2. 사용자 정의 복사 가능 클래스
class Copyable {
 public:
  Copyable(const Copyable& other) = default;  // 복사 생성자
  Copyable& operator=(const Copyable& other) = default;  // 복사 대입 연산자
};

 

 

2. 이동 가능(Movable)한 객체

  • 임시 객체(temporary object)로부터 데이터를 가져올 수 있는 객체
  • 이동이 가능한 경우, 복사보다 빠르게 데이터를 전송할 수 있음
  • 이동 이후 원본 객체의 상태는 “비워진(empty)” 상태 (이전 데이터를 다른 객체가 가져갔기 때문)
  • 예시 1. 이동 가능한 객체 (std::unique_ptr)
std::unique_ptr<int> p1 = std::make_unique<int>(10);
std::unique_ptr<int> p2 = std::move(p1);  // ✅ 이동 가능 (p1은 더 이상 10을 소유하지 않음)
  • 예시 2. 사용자 정의 이동 가능 클래스
 public:
  MoveOnly(MoveOnly&& other) = default;  // 이동 생성자
  MoveOnly& operator=(MoveOnly&& other) = default;  // 이동 대입 연산자

  MoveOnly(const MoveOnly&) = delete;  // 복사는 금지
  MoveOnly& operator=(const MoveOnly&) = delete;  // 복사는 금지
};

 

3. 객체 슬라이싱 문제

  • 기반 클래스(base class)를 복사할 때, 파생 클래스(derived class)의 정보가 손실되는 현상
  • 예시 1.
class Base {
 public:
  virtual void Print() { std::cout << "Base\n"; }
};

class Derived : public Base {
 public:
  void Print() override { std::cout << "Derived\n"; }
};

void Func(Base b) {  // ❌ 복사 생성자로 인해 slicing 발생
    b.Print();  // 항상 Base::Print() 호출됨
}

int main() {
    Derived d;
    Func(d);  // "Base"가 출력됨 (slicing 발생)
}
  • 예시 2. 해결 방법 (포인터 또는 참조 사용)
void Func(Base& b) {  // ✅ 참조를 사용하면 slicing 방지
    b.Print();
}

Inheritance

모든 상속(inheritance)은 public 으로 선언되어야 한다.

  • private 상속을 수행하려면 기본 클래스의 인스턴스를 구성원으로 대신 포함
  • 기본 클래스로 사용하는 것을 지원하지 않을 경우 final 키워드를 클래스 앞에 선언

상속의 사용을 "is-a"의 경우로 제한한다.

  • "Bar가 Foo의 한 종류" 라고 합리적으로 말할 수 있어야 함

인터페이스(interface) 상속은 virtual 클래스로 부터의 상속을 말한다.

  • 그 외는 구현(Implementation) 상속이라고 말함

하위 클래스에서 액세스해야 할 수 있는 멤버 함수로 protected의 사용을 제한한다.

다중 상속은 허용되지만 다중 구현 상속은 권장되지 않는다.

Access Control

상수가 아닌 경우 클래스의 데이터 멤버를 private으로 선언한다.

Declaration order

유사한 선언을 그룹화하고 public 부분을 먼저 배치한다.

클래스의 정의는 일반적으로 public: 섹션으로 시작하고 그 다음에 protected:, private: 가 따라와야 한다.

  • 비어 있는 섹션은 생략함

다음 순서를 선호한다.

  1. Types and type aliases (typedef, using, enum, nested structs and classes, and friend types)
  2. (Optionally, for structs only) non-static data members
  3. Static constants
  4. Factory functions
  5. Constructors and assignment operators
  6. Destructor
  7. All other functions (static and non-static member functions, and friend functions)
  8. All other data members (static and non-static)

+ Recent posts