Formatting

모든 사람이 동일한 스타일을 사용하면 프로젝트를 훨씬 더 쉽게 따라갈 수 있다.

Line Length

한 줄의 길이는 최대 80자로 제한한다. (1960년대 기준)

하지만 다음과 같은 경우에는 80자를 초과할 수 있다.

  • 긴 URL이나 명령어가 포함된 주석 (잘못 나누면 복사, 붙여넣기가 어려움)
  • string literal 내의 중요한 데이터 (URL, JSON, SQL 등)
  • #include 문, 헤더 가드, using 선언

사용성/검색성과 가독성 사이에서 균형을 맞춰야 한다.

Non-ASCII Characters

되도록 사용을 피하되 필요한 경우 UTF-8 인코딩을 사용한다.

단, char16_t, char32_t, wchar_t는 사용하지 않는다.

std::string greeting = u8"こんにちは";  // 일본어 "안녕하세요"
std::string bom = "\xEF\xBB\xBF";  // UTF-8 BOM(Byte Order Mark)
std::string text = "안녕하세요";  // ✅ UTF-8 인코딩 사용

char16_t text[] = u"안녕하세요";  // ❌ char16_t 사용 금지
wchar_t text[] = L"Hello";       // ❌ wchar_t 사용 금지

Spaces vs. Tabs

들여쓰기는 space 2칸을 사용하며, tab은 절대 사용하지 않는다.

  • 에디터를 설정하여 Tab 키를 누르면 space가 삽입되도록 한다.

Function Declarations and Definitions

함수 선언과 정의는 가독성을 고려하여 적절히 정렬해야 한다.

  • 반환 타입은 함수 이름과 같은 줄에 배치하는 것이 원칙이다.
  • 매개변수가 많아 한 줄에 다 들어가지 않으면 줄을 나누어 정렬해야 한다.
  • 예시 1. 매개변수가 많을 경우 줄 나누기
ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2,
                                             Type par_name3) {
  DoSomething();
}
  • 예시 2. 함수 이름이 너무 길 경우
ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
    Type par_name1,  // 4 space indent
    Type par_name2,
    Type par_name3) {
  DoSomething();  // 2 space indent
}

[[nodiscard]], ABSL_ATTRIBUTE_NOINLINE 같은 속성(Attribute)은 반환 타입 앞에 배치한다.

[[nodiscard]] bool IsValid();
ABSL_ATTRIBUTE_NOINLINE void ExpensiveFunction();

Lambda Expressions

람다의 매개변수와 본문은 일반 함수와 동일한 스타일을 적용해야 한다.

  • 짧은 람다는 인라인으로 사용할 수 있다.
  • 참조 캡쳐(&x)에는 공백을 두지 않는다.
int x = 0;
auto x_plus_n = [&x](int n) -> int { return x + n; };
  • 예시 1. 여러 줄 람다를 정렬
digits.erase(std::remove_if(digits.begin(), digits.end(),
                            [&to_remove](int i) {
                              return to_remove.contains(i);
                            }),
             digits.end());
  • 예시 2. 잘못된 예시 (들여쓰기 불일치)
digits.erase(std::remove_if(digits.begin(), digits.end(),
                            [&to_remove](int i) { return to_remove.contains(i); }),
             digits.end());  // ❌ 너무 길어서 가독성이 떨어짐.

Floating-point Literals

부동소수점에는 항상 소수점(radix point)를 포함해야한다.

  • 소수점 앞 뒤에는 항상 숫자가 있어야 함 (.5 ❌ → 0.5 ✅)
  • 지수 표기법(Exponential Notation)에서도 소수점을 포함 (1e6 ❌ → 1.0e6 ✅)
  • 정수를 부동소수점 변수에 할당하는 것은 가능하지만, 명확성을 위해 .0을 추가하는 것이 좋음
  • float, double, long double 타입에 맞는 접미사(f, L)를 사용

Function Calls

가능한 경우 함수 호출은 한 줄에 작성한다.

bool result = DoSomething(argument1, argument2, argument3); // ✅

bool result = DoSomething(
    argument1, argument2, argument3);  // ❌ 불필요한 줄 나누기

모든 인자가 한 줄에 들어가지 않을 경우, 첫 번째 인자와 정렬하거나 4칸 들여쓰기 한다.

// 첫 번째 인자와 정렬
bool result = DoSomething(averyveryveryverylongargument1,
                          argument2, argument3);
// 4칸 들여쓰기
bool result = DoSomething(
    argument1, argument2,  // 4 space indent
    argument3, argument4);

여는 괄호 ( 뒤와 닫는 괄호 ) 앞에는 공백을 추가하지 않는다.

가독성을 높이기 위해 복잡한 인자는 별도의 변수에 할당할 수도 있다.

bool result = DoSomething(scores[x] * y + bases[x], x, y, z);  // ❌ 가독성 낮음

// ✅ 1. 변수로 정리
int my_heuristic = scores[x] * y + bases[x];
bool result = DoSomething(my_heuristic, x, y, z);

// ✅ 2. 주석으로 의미 설명
bool result = DoSomething(scores[x] * y + bases[x],  // Score heuristic.
                          x, y, z);

행렬과 같이 논리적인 그룹이 있는 경우, 그 구조를 유지하면서 정렬할 수 있다.

// Transform the widget by a 3×3 matrix.
my_widget.Transform(x1, x2, x3,
                    y1, y2, y3,
                    z1, z2, z3);

Braced Initializer List Format

중괄호 {} 초기화는 함수 호출과 동일한 스타일을 적용해야 한다.

한 줄에 표현할 수 있으면 한 줄로 유지한다.

return {foo, bar};
functioncall({foo, bar});
std::pair<int, int> p{foo, bar};

여러 줄로 나누어야 할 경우, 첫 번째 요소와 정렬하거나 4칸 들여쓰기 한다.

// When you have to wrap.
SomeFunction(
    {"assume a zero-length name before {"},
    some_other_function_parameter);
    
SomeType variable{
    some, other, values,
    {"assume a zero-length name before {"},
    SomeOtherType{
        "Very long string requiring the surrounding breaks.",
        some, other, values},
    SomeOtherType{"Slightly shorter string",
                  some, other, values}};
                  
SomeType variable{
    "This is too long to fit all in one line"};

중첩된 리스트는 논리적인 그룹을 유지하면서 정렬해야 한다.

명확성을 위해 필요하면 중괄호 {}를 함수 호출처럼 정렬한다.

Looping and branching statements

if, else, switch, while, do, for 등의 키워드는 띄어쓰기를 포함해야 한다.

조건문 또는 반복문의 괄호 ( ) 내부에는 공백을 추가하지 않는다.

중괄호 {}는 항상 사용해야 하며, 열리는 { 뒤와 닫히는 } 앞에는 한 줄씩 추가한다.

  • 예시 1. 올바른 예시
if (condition) {  // ✅ 공백 형식 유지
  DoOneThing();
  DoAnotherThing();
} else if (int a = f(); a != 3) {  // ✅ 초기화된 변수 포함 가능
  DoAThirdThing(a);
} else {
  DoNothing();
}

// Good - the same rules apply to loops.
while (condition) {
  RepeatAThing();
}

// Good - the same rules apply to loops.
do {
  RepeatAThing();
} while (condition);

// Good - the same rules apply to loops.
for (int i = 0; i < 10; ++i) {
  RepeatAThing();
}
  • 예시 2. 잘못된 예시
if(condition) {}    // ❌ `if` 뒤에 공백 없음
else if ( condition ) {}  // ❌ 괄호 안쪽에 공백이 포함됨
else if(condition){}  // ❌ 여러 곳에서 공백 누락

if (condition)
  foo;  // ❌ 중괄호 `{}` 없으면 가독성이 떨어지고, 버그 가능성 증가
else {
  bar;
}

// Bad - `if` statement too long to omit braces.
if (condition)
  // Comment
  DoSomething();

// Bad - `if` statement too long to omit braces.
if (condition1 &&
    condition2)
  DoSomething();

if ... else, do ... while 문은 한 줄로 축약할 수 없다.

  • 간결한 경우 예외적으로 한줄로 표현 가능
if (x == kFoo) return new Foo();  // ✅ 짧고 간결한 경우만 허용
if (x == kBar)
  Bar(arg1, arg2, arg3);  // ✅ 한 줄 조건문, 다음 줄 실행문 가능

switch 문에서 case 블록은 {}를 포함할 수도 있고 생략할 수도 있다.

빈 루프(Empty Loop)는 {} 또는 continue;를 사용해야 한다.

while (condition) {}  // Good - `{}` indicates no logic.
while (condition) {
  // Comments are okay, too
}
while (condition) continue;  // Good - `continue` indicates no logic.

Pointer and Reference Expressions

멤버 접근 연산자(. 및 →) 주변에는 공백을 추가하지 않는다.

포인터(*), 참조(&)연산자는 뒤에 공백을 두지 않는다.

x = *p;
p = &x;
x = r.y;
x = r->y;

포인터 및 참조 선언에서 * 및 & 의 위치는 앞 또는 뒤에 둘 수 있다.

  • 일관성을 유지하는게 중요함

같은 선언 문에서 포인터/참조 변수와 일반 변수를 함께 선언하지 않는다.

int x, *y;  // ❌ 일반 변수와 포인터 변수 혼합 선언 금지
int* x, *y;  // ❌ `*`가 변수별로 다르게 적용되어 가독성 저하

템플릿에서 *> 처럼 * 뒤 공백을 생략해야 한다.

// These are fine, space preceding.
char *c;
const std::string &str;
int *GetPointer();
std::vector<char *>

// These are fine, space following (or elided).
char* c;
const std::string& str;
int* GetPointer();
std::vector<char*>  // Note no space between '*' and '>'

Boolean Expressions

긴 표현식을 줄바꿈할 때 연산자(&&, ||)의 위치를 일관되게 유지해야 한다.

연산자는 일반적으로 줄 끝에 배치하지만, 줄 시작에 배치하는 것도 허용된다.

불필요한 괄호 사용을 피하면서도, 가독성을 높이기 위해 적절한 괄호를 추가할 수 있다.

&&, || 등의 연산자는 and, or 같은 키워드 대신 기호(&&, ||)를 사용한다.

if (this_one_thing > this_other_thing &&
    a_third_thing == a_fourth_thing &&
    yet_another && last_one) {
  DoSomething();
}

if (this_one_thing > this_other_thing
    && a_third_thing == a_fourth_thing
    && yet_another && last_one) {  // ❌ 연산자가 줄 시작에 배치됨 -> 가독성 하락
  DoSomething();
}

Return Values

단순한 return 표현식에서는 불필요한 괄호를 사용하지 않는다.

return 문에서 괄호는 복잡한 표현식을 명확하게 하는 경우에만 사용한다.

return (some_long_condition &&
        another_condition);

return은 함수가 아니므로 return(result); 같은 표현은 잘못된 스타일이다.

  • return(result); ❌ → return result; ✅

Variable and Array Initialization

변수 초기화 시 =, (), {} 세 가지 스타일을 선택할 수 있다.

std::initializer_list 생성자가 있는 경우 {} 초기화 방식이 예상과 다르게 동작할 수 있다.

std::vector<int> v(100, 1);  // ✅ 100개의 요소가 1로 채워진 벡터
std::vector<int> v{100, 1};  // ✅ 2개의 요소를 가진 벡터 (100, 1)

std::vector<int> v{100};  // ❌ 벡터 크기 100을 의도했으나, 원소가 1개(100)만 있음

{} 초기화 방식은 암시적 변환(narrowing conversion)을 방지한다.

int pi(3.14);   // ✅ OK, pi == 3
int pi = 3.14;  // ✅ OK, pi == 3

int pi{3.14};  // ❌ 컴파일 오류: narrowing conversion 금지

기본 생성자를 호출하려면 {} 또는 ()를 사용할 수 있다.

std::vector<int> v1;     // ✅ 기본 생성자 호출 (빈 벡터)
std::vector<int> v2{};   // ✅ 기본 생성자 호출 (빈 벡터)
std::vector<int> v3();   // ❌ 함수 선언으로 해석됨 (Most Vexing Parse)

Preprocessor Directives

#include, #define, #ifdef, #endif 등 반드시 줄의 시작 부분에서 시작해야 한다.

코드 블록 내에 있어도 전처리기 지시문은 절대 들여쓰기(indentation)하지 않는다.

# 뒤에는 공백을 넣을 수도 있지만, 일반적으로 넣지 않는 것이 권장된다.

#if ~ #endif 블록을 사용할 때는 일관된 스타일을 유지해야 한다.

if (lopsided_score) {
#if DISASTER_PENDING  // ✅ 줄의 시작에서 시작
  DropEverything();
# if NOTIFY          // ✅ `#` 뒤에 공백을 넣어도 되지만 권장되진 않음
  NotifyClient();
# endif
#endif
  BackToNormal();
}

if (lopsided_score) {
    #if DISASTER_PENDING  // ❌ `#if`가 들여쓰기 됨
    DropEverything();
    #endif                // ❌ `#endif`도 들여쓰기 됨
    BackToNormal();
}

Class Format

public:, protected:, private: 키워드는 한 칸 들여쓰기한다.

public: → protected: → private: 순서로 배치한다.

public:, protected:, private: 키워드 앞에 빈 줄을 추가할 수 있지만, 첫 번째는 제외한다.

public: 이후 빈 줄을 두지 않는다.

기본적인 들여쓰기는 2칸을 사용한다.

class MyClass : public OtherClass {
 public:      // Note the 1 space indent!
  MyClass();  // Regular 2 space indent.
  explicit MyClass(int var);
  ~MyClass() {}

  void SomeFunction();
  void SomeFunctionThatDoesNothing() {
  }

  void set_some_var(int var) { some_var_ = var; }
  int some_var() const { return some_var_; }

 private:
  bool SomeInternalFunction();

  int some_var_;
  int some_other_var_;
};

클래스 상속 시, 베이스 클래스는 동일한 줄에 배치하며, 80자 제한을 고려한다.

  • 상속받을 때도 public→ protected→ private 순서를 유지
class DerivedClass : private BaseClass, public AnotherBase {  // ❌ `public`이 먼저 와야 함
 public:
  void SomeFunction();
};

Constructor Initializer Lists

모든 초기화 목록이 한 줄에 들어가면 그대로 유지한다.

MyClass::MyClass(int var) : some_var_(var) { // ✅ 올바른 예시 (한 줄 유지)
  DoSomething();
}

MyClass::MyClass(int var) 
    : some_var_(var) {  // ❌ 한 줄에 충분히 들어가므로 줄바꿈 불필요
  DoSomething();
}

여러 줄로 나눠야 할 경우, :(콜론) 앞에서 줄바꿈하고 4칸 들여쓰기한다.

MyClass::MyClass(int var)
    : some_var_(var), some_other_var_(var + 1) { // ✅ 올바른 예시 (줄바꿈 후 4칸 들여쓰기)
  DoSomething();
}

MyClass::MyClass(int var) : 
    some_var_(var), some_other_var_(var + 1) {  // ❌ `:` 뒤에서 줄바꿈 금지
  DoSomething();
}

여러 개의 멤버를 초기화하는 경우, 각 멤버를 개별 줄에 배치하고 정렬한다.

중괄호 {}는 한 줄에 배치할 수도 있고, 새로운 줄에 배치할 수도 있다.

  • 중괄호 {}는 :과 동일한 줄에 위치해야 함
MyClass::MyClass(int var) : some_var_(var) {}

MyClass::MyClass(int var)
    : some_var_(var) 
    {
  DoSomething();
}  // ❌ `{`의 위치가 잘못됨

Namespace Formatting

네임스페이스 내부의 코드(함수, 클래스, 변수 등)는 들여쓰기하지 않는다.

namespace {

void foo() {  // ✅ 네임스페이스 내부지만 추가 들여쓰기 없음
  DoSomething();
}

}  // namespace

네임스페이스 {}는 일반적인 코드 블록처럼 취급하지만, 내부 내용은 평소대로 들여쓴다.

namespace my_namespace {

void foo() {
  if (condition) {
    DoSomething();
  }
}

}  // namespace my_namespace

여러 개의 네임스페이스가 중첩될 경우, 들여쓰기 없이 같은 규칙을 적용한다.

namespace outer_namespace {
namespace inner_namespace {

void foo() {
  DoSomething();
}

}  // namespace inner_namespace
}  // namespace outer_namespace

Horizontal Whitespace

한 줄 끝에는 공백(trailing whitespace)을 추가하지 않는다.

// 주석 앞에는 공백 두 칸을 추가한다.

int i = 0;  // ✅ 공백 두 칸 유지
int i = 0;// ❌ 공백이 없음
int i = 0; // ❌ 공백 하나만 있음

{} 중괄호 앞에는 공백을 추가한다.

void f(bool b) {  // ✅ `{` 앞에 공백 포함
  ...
}

void f(bool b){  // ❌ `{` 앞에 공백이 없음
  ...
}

int x[] = { 0 };   // ✅ `{}` 내부 공백을 넣을 수도 있음
int x[] = {0};     // ✅ `{}` 내부 공백을 제거할 수도 있음

:(콜론)은 상속 및 초기화 목록에서 공백을 포함해야 한다.

class Foo : public Bar {  // ✅ `:` 앞뒤에 공백 포함
};

Foo(int b) : Bar(), baz_(b) {}  // ✅ `:` 앞뒤에 공백 포함

class Foo:public Bar {  // ❌ `:` 앞뒤에 공백 없음
};

if, while, for 등의 키워드 뒤에는 공백을 추가한다.

if (b) {  // ✅ `if` 뒤에 공백 포함
  ...
}

while (test) {  // ✅ `while` 뒤에 공백 포함
  ...
}

for (int i = 0; i < 5; ++i) {  // ✅ `for` 뒤에 공백 포함
  ...
}

for (auto x : counts) {  // ✅ `:` 앞뒤 공백 포함
  ...
}

if(b) {  // ❌ `if` 뒤에 공백 없음
  ...
}

if  (b) {  // ❌ `if` 뒤에 공백이 두 개 이상 있음
  ...
}

연산자(=, +, -, *, /)는 일반적으로 공백을 포함하지만, 곱셈과 나눗셈은 예외 가능하다.

x = 0;  // ✅ `=` 연산자 양쪽 공백 포함
v = w * x + y / z;  // ✅ `*`, `/` 연산자는 생략 가능
v = w*x + y/z;  // ✅ `*`, `/` 연산자 붙여도 허용됨

x=0;  // ❌ 공백 없음

템플릿 및 캐스트 연산에서 < > 사이에는 공백을 넣지 않는다.

std::vector<std::string> x;  // ✅ `< >` 내부 공백 없음
std::vector< std::string > x;  // ❌ `< >` 내부 공백 있음

y = static_cast<char*>(x);  // ✅ `>(` 사이 공백 없음
y = static_cast<char*> (x);  // ❌ `>(` 사이 공백 포함됨

Vertical Whitespace

불필요한 빈 줄(blank lines)은 최소화해야 한다. (필요한 곳에서만 사용)

함수 시작과 끝에 빈 줄을 추가하지 않는다.

if-else 블록 내부에서는 가독성을 위해 빈 줄을 적절히 사용할 수 있다.

주석 앞에는 빈 줄을 추가하여 주석이 뒤의 코드와 연결되도록 한다.

DoSomething();

// 다음 단계에서 값을 초기화한다.
InitializeValues();

네임스페이스 선언 내부에서 첫 번째 선언과 구분을 위해 빈 줄을 추가할 수 있다.

namespace my_namespace {

// 첫 번째 함수 선언
void Foo();
void Bar();

}  // namespace my_namespace

주석이 매우 중요하지만, 최고의 코드는 자체 문서화이다.

변수에 합리적인 이름을 부여하는 것이 주석을 통해 설명해야 하는 모호한 이름을 사용하는 것보다 훨씬 낫다.

다음 규칙들은 무엇을 어디에 댓글을 달아야 하는지를 설명한다.

Comment Style

// 또는 /* */ 구문을 일관성있게 사용한다.

  • // 가 훨씬 더 일반적
  • 어떤 스타일을 어디에 사용하는지 일관성을 유지

File Comments

모든 파일의 맨 위에 라이선스 주석(license boilerplate)을 작성한다.

  • 프로젝트에 따라 Apache, BSD, MIT, GPL 등 적절한 라이선스를 선택

헤더 파일에 여러 개의 관련 클래스를 포함하는 경우, 해당 파일이 어떤 개념을 다루는지 설명해야 한다.

  • 파일의 역할을 한 문단으로 설명
  • “이 파일이 무엇을 포함하는지” 명확히 서술
  • 파일에 추가할 내용과 추가하면 안될 내용을 정리하면 나중에 관리하기 용이해짐
// frobber.h - Defines the Frobber class and related utilities.
//
// This file provides an abstraction for frobbing data structures
// in an efficient manner. It includes:
//   - `Frobber`: A base class for handling frobbing operations
//   - `FrobberFactory`: A factory class for creating `Frobber` instances
//
// If you need additional frobbing utilities, consider adding them here.
#ifndef FROBBER_H_
#define FROBBER_H_

class Frobber { ... };

class FrobberFactory { ... };

#endif  // FROBBER_H_

일반적으로 .cc 파일은 헤더 파일의 구현체이므로, 주석이 필요하지 않다.

  • 그러나 헤더 파일이 없는 .cc 파일이라면, 개념을 설명하는 주석을 추가해야함

파일 상단에 author 라인을 포함하지 않는다.

  • 팀 프로젝트에서는 여러 사람이 코드를 수정하므로 author 라인이 불필요함
  • Git 과 같은 버전 관리 시스템이 변경 기록을 추적하므로, 따로 작성할 필요가 없음
  • 예시 1. 잘못된 주석 (author 라인 포함)
// Copyright 2024 MyProject Developers
// Author: John Doe
//
// Licensed under the Apache License, Version 2.0 (the "License")...

Struct and Class Comments

무엇을 위한 것인지, 어떻게 사용해야 하는지를 설명하는 주석이 첨부되어야 한다.

  • 예시 1.
// Iterates over the contents of a GargantuanTable.
// Example:
//    std::unique_ptr<GargantuanTableIterator> iter = table->NewIterator();
//    for (iter->Seek("foo"); !iter->done(); iter->Next()) {
//      process(iter->key(), iter->value());
//    }
class GargantuanTableIterator {
  ...
};
  • 예시 2.
// Stores configuration settings for the application.
struct Config {
  int timeout;  // Request timeout in milliseconds.
  bool enable_feature;  // Whether to enable the experimental feature.
};

Function Comments

함수 선언부(헤더 파일 .h)와 정의부(구현 파일 .cc)의 주석은 각각 다른 목적을 가진다.

  • 선언부 주석 : 함수의 사용법(입출력, 제약 조건, 예제 등)을 설명
  • 정의부 주석 : 함수의 내부 동작 방식과 구현상의 중요한 고려사항을 설명

단순한 Getter/Setter 함수는 주석을 생략한다.

함수가 포인터를 받으면 nullptr 허용 여부를 명확하게 설명한다.

생성자와 소멸자는 특별한 동작이 없다면 주석을 생략한다.

Variable Comments

변수 이름은 그 목적을 설명할 수 있도록 직관적으로 작성해야 한다.

클래스 멤버 변수는 이름만으로 충분한 의미를 전달할 수 있으면 주석을 생략할 수 있다.

nullptr, -1 등의 센티널 값(Sentinel Values) 을 사용할 경우, 반드시 의미를 설명해야 한다.

전역 변수(Global Variables)는 반드시 주석을 포함해야 하며, “왜 전역 변수로 선언했는지” 명확히 설명해야 한다.

Implementation Comments

어렵거나 복잡하거나 중요한 부분에는 반드시 코드 앞에 주석을 추가해야 한다.

// Uses a binary search to find the insertion point for the new element.
// This ensures O(log N) insertion time in a sorted list.
auto it = std::lower_bound(sorted_list.begin(), sorted_list.end(), new_element);
sorted_list.insert(it, new_element);

함수 인자의 의미가 명확하지 않으면 개선 방법을 고려해야 한다.

  • 예시 1. literal constant 제거, 명명된 상수 사용
// const DecimalNumber product = CalculateProduct(values, 7, false, nullptr);

constexpr int kDefaultPrecision = 7;
constexpr bool kDisableCache = false;

const DecimalNumber product = CalculateProduct(values, kDefaultPrecision, kDisableCache, nullptr);

코드가 자체적으로 의미를 전달 할 수 있도록 노력하고, 불필요한 주석은 피해야 한다.

  • 예시 1. std::find() 는 원래 특정 요소를 찾는 함수이므로 주석이 불필요
// Find the element in the vector.
if (std::find(v.begin(), v.end(), element) != v.end()) {
  Process(element);
}
  • 예시 2. 주석으로 코드의 의도를 설명(”이전에 처리된 요소는 다시 처리하지 않는다”
// Process "element" unless it was already processed.
if (std::find(v.begin(), v.end(), element) != v.end()) {
  Process(element);
}

Self-Describing Code 활용 (코드가 스스로 의미를 전달할 수 있도록)

if (!IsAlreadyProcessed(element)) {
  Process(element);
}

Punctuation, Spelling, and Grammar

구두점, 철자, 문법을 올바르게 사용해서 가독성이 좋아야 한다.

int count = 0;  // Counts the number of processed elements.

코드 리뷰에서 주석을 지적 받는 것이 불편하더라도 최종적으로는 코드의 가독성에 도움이 된다.

// Intializes the buffer and sets its size.  // ❌ 'Intializes' → 'Initializes'
buffer.Init(size);

TODO Comments

TODO 주석은 임시 코드, 단기 해결책, 또는 완벽하지 않은 코드에서 사용해야 한다.

TODO 는 항상 대문자로 작성하며, 이후에 관련 정보를 포함해야 한다.

TODO에 버그 ID, 담당자 이름, 이메일, 또는 관련 문서를 포함해야 한다.

// TODO: bug 12345678 - Remove this after the 2047q4 compatibility window expires.
// TODO: example.com/my-design-doc - Manually fix up this code the next time it's touched.
// TODO(bug 12345678): Update this list after the Foo service is turned down.
// TODO(John): Use a "\*" here for concatenation operator.

단순한 문구보다는 언제 해결해야 하는지 명확한 조건(날짜 또는 특정 이벤트)을 포함해야 한다.

  • Fix by November 2005 , Remove this code when all clients can handle XML responses.

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)

유튜브를 해보겠다고 야심차게 구독했던 어도비,

당분간은 못할 것 같아서 취소를 하기로 마음먹었다.

 

adobe에 로그인하고 오른쪽 상단에 프로필을 누른다.

그리고 계정 관리를 클릭한다.

우측 계정 관리 클릭

 

좌측 하단에 플랜 관리를 누른다. 

플랜 관리

 

그리고 플랜 변경을 선택한다.

플랜 취소

 

그럼 지금 쓰는 것보다 싼 라이트룸 플랜을 선택한다.

플랜 선택

기존 어도비 요금제가 선택한 것보다 비싼거여서 부분 환불을 해준다.

인증단계로 진행

 

그럼 이제 변경되었다.

 

 

그리고 한 30분정도 있으면,

아까 플랜 관리에서 취소 버튼이 생긴다.

취소하자!

 

그럼 이제 취소가 된다.

 

조기 취소 수수료 없이 해지 완료!

Scoping

Namespaces

대부분의 코드가 합리적으로 짧은 이름을 사용할 수 있도록 하는 동시에 이름 충돌을 방지하는 방법을 제공한다.

  • 몇가지 예외를 제외하고는 네임스페이스에 코드를 배치한다
  • 가능한 프로젝트 이름과 경로를 기준으로 고유한 이름을 가져야 한다.
  • 아래 예제와 같이 주석을 달아 네임스페이스 종료를 알린다.
// In the .h file
namespace mynamespace {
 
// All declarations are within the namespace scope.
// Notice the lack of indentation.
class MyClass {
 public:
  ...
  void Foo();
};
 
}  // namespace mynamespace
// In the .cc file
namespace mynamespace {
 
// Definition of functions is within scope of the namespace.
void MyClass::Foo() {
  ...
}
 
}  // namespace mynamespace
  • inline namespace를 가급적 사용하지 않고, 꼭 사용해야한다면 아래의 예시처럼 default 인식에 주의한다.
    • 버전 관리같은 의도된 default 동작은 예외
    namespace CAR {
    namespace VER1 {
    class ElectricCar; // VER1 Class
    }  // namespace VER1
    
    inline namespace VER2 {
    class ElectricCar; // VER2 Class
    }  // namespace VER2
    
    class GasCar;
    
    }  // namepsace CAR
    
    // CAR::VER2::ElectricCar, VER2가 inline namespace이기 때문에 CAR namespace로 간주
    CAR::ElectricCar* car1;
    CAR::VER1::ElectricCar* car2;
    CAR::VER2::ElectricCar* car3;
    
  • using namespace 사용에 주의한다.
// Forbidden -- This pollutes the namespace.
using namespace foo;

// Shorten access to some commonly used names in .cc files.
namespace baz = ::foo::bar::baz;

 

Internal Linkage

함수, 변수 등이 해당 소스 파일(.cc)에서만 사용될 경우, 다른 파일에서 접근하지 못하도록 내부 연결을 부여한다.

  • Unnamed namespace 사용
namespace {
    int internalVar = 42;  // 다른 파일에서 접근 불가능
    void internalFunction() {
        // ...
    }
}  // namespace
  • static 키워드 사용
static int internalVar = 42;  // 다른 파일에서 접근 불가능
static void internalFunction() {
    // ...
}

내부 연결은 헤더 파일에서는 사용하 안된다.

  • 각각의 소스 파일에서 서로 다른 복사본이 생성되기 때문

Nonmember, Static Member, and Global Functions

비 멤버 함수와 정적 멤버 함수, 글로벌 함수의 적절한 사용이 필요하다.

  • 클래스에 속하지 않은 함수, Nonmember function 는 네임스페이스 안에 넣는다.
  • 글로벌 함수보다 네임스페이스를 활용한다.
  • 예시 1. 글로벌 네임스페이스의 오염
#include <iostream>

void PrintMessage() {  // 전역(global) 네임스페이스에 있음
    std::cout << "Hello, world!" << std::endl;
}
  • 예시 2. 올바른 예
#include <iostream>

namespace utils {
    void PrintMessage() {
        std::cout << "Hello, world!" << std::endl;
    }
}

int main() {
    utils::PrintMessage();
    return 0;
}

 

  • 정적 멤버 변수를 위한 클래스는 사용하지 않는다.
    • 예시 1. 클래스를 네임스페이스처럼 사용
class Utility {
public:
    static void PrintMessage() {
        std::cout << "Hello, world!" << std::endl;
    }
};

 

  • ODR(One Definition Rule) 문제 발생에 주의한다.
    • 같은 프로그램에서 동일한 이름의 함수가 여러 번 정의되면 컴파일 오류 발생
// file1.cpp
#include <iostream>
void PrintMessage() {  // 글로벌 함수
    std::cout << "Hello from file1!" << std::endl;
}

// file2.cpp
#include <iostream>
void PrintMessage() {  // 글로벌 함수 (중복 정의!)
    std::cout << "Hello from file2!" << std::endl;
}

// ❌ 링크 오류 발생! (ODR 위반)

Local Variables

가능한 가장 좁은 범위에 함수의 변수를 배치하고 선언에서 변수를 꼭 초기화한다.

  • C++ 에서는 함수의 어느 곳에서나 변수를 선언할 수 있다.
  • 처음 사용하는 곳에 가깝게 선언하는 것이 좋다.
    • 선언을 찾고 변수 유형과 초기화 대상을 쉽게 확인할 수 있음
    • 선언 및 할당 대신 초기화를 사용
      std::vector<int> v;
      v.push_back(1);  // Prefer initializing using brace initialization.
      v.push_back(2);
      
      std::vector<int> v = {1, 2};  // Good -- v starts initialized.
      
      std::vector<int> v;
      v.push_back(1);  // Prefer initializing using brace initialization.
      v.push_back(2);
      
      std::vector<int> v = {1, 2};  // Good -- v starts initialized.
  • If, while 및 for 문에 필요한 변수는 일반적으로 해당 statement 내에서 선언되어야 하며, 이러한 변수는 해당 범위로 제한된다.
while (const char* p = strchr(str, '/')) str = p + 1;
  • 변수가 객체인 경우, 해당 루프 외부에서 선언하는 것이 더 효율적이다.
// Inefficient implementation:
for (int i = 0; i < 1000000; ++i) {
  Foo f;  // My ctor and dtor get called 1000000 times each.
  f.DoSomething(i);
}

Foo f;  // My ctor and dtor get called once each.
for (int i = 0; i < 1000000; ++i) {
  f.DoSomething(i);
}

 

Static and Global Variable

정적 저장 수명(static storage duration)을 가진 객체는 프로그램이 시작될 때부터 종료될 때까지 존재하는 객체이다. 이러한 객체는 아래와 같은 경우에 생성된다.

  • 네임스페이스 수준의 전역 변수(global variable)
  • 클래스의 static 멤버 변수
  • 정적 함수 지역 변수(static function-local variable)
// 네임스페이스 스코프 (전역 변수)
const int globalValue = 42;  

// 클래스의 static 멤버 변수
class MyClass {
    static int classStaticVar;
};

// 함수 내부의 static 변수
void foo() {
    static int localStaticVar = 10;
}

이러한 변수들은 종료시 소멸되지만 여러 가지 문제를 유발할 수 있다.

  • 예시 1. 나쁜 예
// bad: non-trivial destructor
const std::string kFoo = "foo";

// Bad for the same reason, even though kBar is a reference (the
// rule also applies to lifetime-extended temporary objects).
const std::string& kBar = StrCat("a", "b", "c");

void bar() {
  // Bad: non-trivial destructor.
  static std::map<int, int> kData = {{1, 0}, {2, 0}, {3, 0}};
}
  • 예시 2. 좋은 예
const int kNum = 10;  // Allowed

struct X { int n; };
const X kX[] = {{1}, {2}, {3}};  // Allowed

void foo() {
  static const char* const kMessages[] = {"hello", "world"};  // Allowed
}

// Allowed: constexpr guarantees trivial destructor.
constexpr std::array<int, 3> kArray = {1, 2, 3};

thread_local Variables

thread_local 지정자를 사용하여 변수를 선언할 수 있다.

thread_local Foo foo = ...;

네임스페이스 범위, 함수 내부 또는 정적 클래스 멤버로 선언할 수 있지만 일반 클래스 멤버로는 선언할 수 없다.

  • 이러한 변수는 실제로 객체의 모임이므로 다른 스레드가 접근 할 때 실제로 다른 객체에 액세스 한다.
  • 변수 인스턴스는 프로그램 시작 시 한 번이 아니라 각 스레드에 대해 별도로 초기화되어야 한다는 점을 제외하고는 정적 변수와 매우 유사하게 초기화된다.
  • 즉, 함수 내에 선언된 thread_local 변수는 안전하지만 다른 thread_local 변수는 정적 변수(와 그 외)와 동일한 초기화 순서 문제의 영향을 받는다.
  • thread_local 변수 인스턴스들은 스레드가 종료되기 전에 소멸되지 않으므로 정적 변수의 소멸 순서 문제가 없다.

Header Files

일반적으로 모든 .cc 파일에는 연관된 .h 파일이 있어야 한다.

  • 단, 유닛 테스트 및 main() 함수만 포함하는 작은 .cc 파일과 같은 예외가 있음

헤더 파일을 올바르게 사용하면 코드의 가독성, 사이즈 및 성능 면에서 큰 차이를 가져올 수 있다.

Self-contained Headers

모든 헤더 파일은 독립적(self-contained)이어야 한다.

  • 여기서 독립적이란, abc.h, abc.cpp따로 파일을 만들어야 한다는 의미이다.
  • 단, template, inline function 에 대한 정의는 선언과 동일한 파일에 배치하는 걸 선호

The #define Guard

모든 헤더 파일에는 다중 포함을 방지하기 위해 #define 이 있어야 한다.

기호 이름의 형식은 PROJECT_PATH_FILE_H_ 여야 한다.

고유성을 보장하려면 프로젝트 소스 트리의 전체 경로를 기반으로 해야 한다.

예를 들어, foo 프로젝트의 foo/src/bar/baz.h 파일에는 아래와 같이 작성한다.

#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_

...

#endif // FOO_BAR_BAZ_H_

Include What You Use

소스 또는 헤더 파일이 다른 곳에 정의된 기호를 참조하는 경우, 파일에는 해당 기호의 선언 또는 정의를 적절하게 제공하려는 헤더 파일이 직접 포함되어야 한다.

즉, 전이적 포함(transitive inclusion)에 의존하면 안된다.

//foo.h
#include "bar.h"
class Foo 
{
public:
    Bar bar;
};
//bar.h
class Bar
{
};
//foo.cc
#include "foo.h"
// #include "bar.h"
int main()
{
    Foo* foo = new Foo();
    Bar* bar = new Bar();
}
  • foo.h 가 bar.h를 포함하기에 .cc 파일에서 foo.h 만 포함하려는 시도

Forward Declaration

가능하면 전방 선언을 사용하지 않는다.

  • 대신 필요한 헤더 파일을 포함해야 함
  • C++ 20 부터는 모듈 시스템의 도입으로 문제점이 완화되었음

Inline Functions

10줄 이하의 함수는 인라인으로 정의한다.

  • loop 또는 switch 의 경우 효율적이지 않음
  • 재귀 함수는 인라인으로 지정하지 않음

Names and Order of Includes

다음의 순서로 포함한다 :
관련 헤더, C 시스템 헤더, C++ 표준 라이브러리 헤더, 기타 라이브러리 헤더, 프로젝트 헤더
프로젝트의 모든 헤더 파일은 UNIX 디렉터리 별칭 .(현재 디렉터리) 또는 ..(상위 디렉터리)를 사용하지 않고 프로젝트 소스 디렉터리의 하위 항목으로 나열되어야 한다.

  • 예시 1. google-awesome-project/src/base/logging.h 는 다음과 같이 포함되어야 한다.

    #include "base/logging.h"
  • 예시 2. google-awesome-project/src/foo/internal/fooserver.cc는 다음과 같을 수 있다:

    #include "foo/server/fooserver.h"
    
    #include <sys/types.h>
    #include <unistd.h>
    
    #include <string>
    #include <vector>
    
    #include "base/basictypes.h"
    #include "base/commandlineflags.h"
    #include "foo/server/bar.h"
  • 예외 1. 조건부 포함

    #include "foo/public/fooserver.h"
    
    #include "base/port.h"  // For LANG_CXX11.
    
    #ifdef LANG_CXX11
    #include <initializer_list>
    #endif  // LANG_CXX11

https://google.github.io/styleguide/cppguide.html

 

Google C++ Style Guide

Google C++ Style Guide Background C++ is one of the main development languages used by many of Google's open-source projects. As every C++ programmer knows, the language has many powerful features, but this power brings with it complexity, which in turn ca

google.github.io

 

라떼썰 같은 꼰대 이야기일지 모르겠으나...
Code convention을 숙지하는 것은 협업의 시작이다.

 

보통 3개월인 수습 기간 동안 숙지할 것을 권장하며

수습이 끝난 후에는 프로젝트 코드에 가장 얕은 권한이 주어진다.

그리고 pull request, code review 를 거치며 코딩 스타일을 체득한다.

 

2022년 chatGPT 이후, 일단 구현해보자가 흐름이라면

앞으로의 이야기는 흐름을 거스른다고 느낄 수도 있겠다.

 

구글 스타일 가이드를 모두 번역할 계획보다는

정말로 필요한 부분만 번역해서 업로드할 계획이다.

 

1. Header Files

2. Scoping

3. Classes

4. Functions

5. Naming

6. Comments

7. Formatting

+ Recent posts