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

+ Recent posts