C/C++ 코딩 스타일

개인적인 표준이자 프로젝트에 표준이 없을 때 제안하는 코딩 스타일이다.

헤더파일

#define 보호문

모든 헤더 파일에는 여러번 포함하지 않도록 #define 보호문을 사용한다.

보호문 이름은 겹치지 않도록 파일 이름을 바탕으로 만든다.

#pragma once를 사용할 수도 있다.

헤더 파일 의존성

객체 멤버 대신 참조나 포인터 또는 스마트 포인터 등으로 전방 선언을 할 수 있으면 #include를 사용하지 않는다. 이 때문에 가독성이 나빠지고 성능이 떨어질 수도 있으므로 목적이 헤더 파일 포함을 최소화하는 것뿐이라면 이렇게 변경하지 않는다.

인라인 함수

함수 내용이 10 줄 이하일 때만 인라인으로 정의한다. 인라인으로 선언하더라도 항상 그렇게 되는 건 아니라는 점을 기억하자. 예를 들면 일반적으로 가상 또는 재귀 함수는 인라인하지 않는다.

이름 포함 순서

가독성을 높이고 숨은 의존성을 피하기 위해 다음 순서로 포함한다. 각 헤더 묶음 내에서는 알파벳 순서로 포함하고, 각 묶음은 빈 줄 하나로 구분한다.

  • (필요하면) 프리컴파일 헤더
  • 구현 파일의 원형/인터페이스 헤더 (cpp 파일에 해당하는 .h 파일)
  • 프로젝트에 속한 같은 디렉터리, 패키지 헤더
  • 프로젝트에 속한 다른 패키지 헤더
  • 프로젝트에 속한 공통 헤더
  • 표준, 시스템에 속하지 않은 라이브러리 헤더 (Qt, Eigen 등)
  • 거의 표준인 라이브러리 (Boost, Catch2 등)
  • 플랫폼 헤더
  • SDK 헤더
  • OS 헤더
  • 표준 C++ 헤더 (iostream 등)
  • 표준 C 헤더 (cstdint, stdio.h 등)

프로젝트에 속한 헤더 파일을 포함하는 경로는 상대 경로 적을 때 사용하는 .(현재 디렉터리) 또는 ..(부모 디렉터리)을 사용하지 않고 해당 프로젝트의 소스 디렉터리 바로 아래부터 시작한다. 예를 들어 project/src/base/logging.h 파일은 다음처럼 적는다.

dir/foo.cpp 또는 dir/foo_test.cpp 파일이 dir2/foo2.h 내용을 구현하거나 테스트하는 것이라면 포함 순서는 다음과 같다.

  1. (필요하면) 프리컴파일 헤더
  2. dir2/foo2.h (선호하는 위치)
  3. 프로젝트에 속한 같은 디렉터리, 패키지 헤더
  4. 프로젝트에 속한 다른 패키지 헤더
  5. 프로젝트에 속한 공통 헤더
  6. 표준, 시스템에 속하지 않은 라이브러리 헤더 (Qt, Eigen 등)
  7. 거의 표준인 라이브러리 (Boost, Catch2 등)
  8. 플랫폼 헤더
  9. SDK 헤더
  10. OS 헤더
  11. 표준 C++ 헤더 (iostream 등)
  12. 표준 C 헤더 (cstdint, stdio.h 등)

예를 들어 project/src/foo/internal/fooserver.cpp 파일에서 포함 순서는 다음과 같을 수 있다.

범위

비멤버, 정적 멤버와 전역 함수

전역 함수보다는 네임스페이스 내 비멤버 함수나 정적 멤버를 우선 고려하자.

지역 변수

변수는 가능한 좁은 범위로 두며 선언할 때 초기화한다.

for, if, while 문 등에서도 같다.

단, 그 변수가 객체이면 범위에 들어갈 때마다 생성자를 호출하고 범위를 벗어날 때마다 소멸자를 호출한다.

이때는 루프 안에서 사용하는 객체를 해당 루프 밖에서 선언하는 게 더 효율적이다.

정적과 전역 변수

생성자에서 무언가를 소멸하는 클래스 타입 정적 또는 전역 변수는 금지한다. 생성자와 소멸자 순서가 정해져 있지 않아 찾기 힘든 버그를 만들 수 있다. 전역 변수, 정적 변수, 정적 클래스 멤버 변수 그리고 함수 정적 변수를 포함해 수명이 정적(static storage duration)인 객체는 POD 타입 또는 생성자에서 아무것도 하지 않는 클래스이어야 한다. 즉 int, char, float 등 기본 타입 또는 포인터나 배열, POD 구조체(생성자, 소멸자 등이 없는 C 구조체)나 소멸자에서 아무것도 하지 않는 클래스 객체여야 한다.

클래스

생성자에서 하는 일

일반적으로 생성자에서는 멤버 변수를 초기화하는 일만 하고 가상 메서드 호출이나 오류를 알리지 못하고 실패하는 초기화는 피한다.

구조체와 클래스

데이터를 저장만 하는 객체에는 구조체를 사용하고 그외에는 모두 클래스를 사용한다. 구조체에는 생성자, 소멸자, initialize(), reset(), validate() 등 데이터 멤버를 설정하는 데 사용하는 메서드 외 다른 것은 없어야 한다.

상속

상속보다는 포함이 더 낫다. 상속은 public으로 한다. 구현 상속을 지나치게 사용하지 않으며 상속은 ‘is-a’ 관계일 때로 한정해 사용한다. protected는 하위 클래스에서 접근할 수도 있는 멤버 함수로 제한해 사용한다. 데이터 멤버는 private이어야 한다.

접근 제어

데이터 멤버는 private으로 하고 필요하면 접근 제어자를 통해 접근하게 한다.

선언 순서

클래스 안에서 선언할 때는 public 다음에 private, 메서드 다음에 데이터 멤버 등과 같은 순서로 한다. 클래스를 정의할 때는 public, protected, private 구역 순으로 하며 비어 있는 구역은 생략한다. 각 구역 안에서는 일반적으로 다음 순서로 한다.

  1. using 선언, using/typedef 타입 별칭, enum
  2. 상수 (static const 데이터 멤버)
  3. 생성자
  4. 소멸자
  5. 메서드 (정적 메서드 포함)
  6. 데이터 멤버 (static const 데이터 멤버 제외)

함수

함수 매개 변수 순서

매개 변수 순서는 입력 전용, 입출력 겸용, 출력 전용 순이다. 함수 매개 변수 순서를 정할 때는 입력 전용 매개 변수 모두를 출력 전용 매개 변수 앞에 둔다. 특히 새 매개 변수를 추가한다는 이유로 함수 끝에 넣지 않으며, 새로 추가하는 입력 전용 매개 변수는 출력 매개 변수 앞에 둔다. 이는 관련 함수와 일관성 때문에 어겨야 할 수도 있다.

함수는 짧게

함수는 짧으면서 한 가지 일에 집중하게 한다. 때로는 긴 함수를 써야 할 수도 있으므로 길이를 강제하진 않으나 약 40 줄을 넘으면 프로그램 구조를 망가뜨리지 않으면서 내용을 나눌 수 없는지 생각해 본다.

명명 규칙

일반적인 명명 규칙

함수 이름, 변수 이름, 파일 이름은 설명적이어야 하므로 축약하지 않는다. 하지만 너무 긴 이름은 쓰기 어렵고 한 줄이 화면 안에 들어오지 않을 정도로 길어지므로 피한다. 타입과 변수는 명사, 함수는 ‘명령형’ 동사로 쓴다.

다음은 잘못된 예이다.

파일 이름

파일 이름은 모두 소문자여야 하고 밑줄을 포함할 수 있다. 사용할 수 있는 파일 이름은 다음과 같다.

C++ 파일은 .cpp로, 헤더 파일은 .h로 마친다. 파일 이름은 매우 한정적으로 만든다. 예를 들면 logs.h보다는 http_server_logs.h를 사용한다.

타입 이름

타입 이름은 대문자로 시작하고 새 단어를 시작할 때마다 대문자를 쓰며 밑줄은 사용하지 않는다. 클래스, 구조체, 타입 별칭, 열거형 등 모든 타입 이름은 명명 규칙이 같다.

변수 이름

변수와 데이터 멤버 이름은 모두 소문자로 쓰고 단어 사이에 밑줄을 쓴다.

일반 변수 이름

클래스 데이터 멤버

정적과 비정적 모두를 포함해 클래스의 private 데이터 멤버는 일반 비멤버 변수와 명명 규칙이 같으나 끝에 밑줄을 붙인다.

구조체 데이터 멤버

정적과 비정적 모두를 포함해 구조체의 public 데이터 멤버는 일반 비멤버 변수와 명명 규칙이 같으나 클래스 데이터 멤버와 달리 끝에 밑줄을 붙이지 않는다.

상수 이름

함수 이름

함수는 소문자로 시작하고 각 단어 사이에 밑줄을 쓸 수 있다.

네임 스페이스 이름

네임스페이스 이름은 모두 소문자이고 프로젝트 이름과 팀, 디렉터리 구조를 바탕으로 한다.

열거형 이름

열거형은 상수처럼 명명한다. 범위 비지정 열거형보다는 범위 지정 열거형을 우선한다.

매크로 이름

매크로는 조건 컴파일 등 꼭 필요할 때 외에는 사용하지 않는다. 만약 꼭 사용해야 한다면 모두 대문자로 하고 단어는 밑줄로 구분한다.

형식

코딩 스타일과 형식은 매우 다양해서 모두가 이 규칙에 동의하지 않을 수 있다. 하지만 일관되게 사용하면 가독성을 높이는 데 도움이 된다. 형식을 정하는 기본 목표는 다음과 같다.

  • 코드의 논리적 구조를 정확히 표현
  • 코드의 논리적 구조를 일관되게 표현
  • 가독성 향상
  • 변경 사항에 대한 내성. 즉 코드 한 줄을 변경한다고 해서 여러 줄을 변경하지 않아야 한다.

줄 길이

각 줄은 최대 80 문자를 넘지 않게 한다. 예외는 다음과 같다.

  • 예로 든 명령이나 URL이 있는 주석문이 80 문자를 넘을 때, 나누면 가독성이 나빠지거나 잘라 붙이기 또는 자동 연결을 쉽게 처리할 수 없을 때는 예외로 한다.
  • 원시 문자열(Raw string) 리터럴은 80 문자를 넘을 수 있다.
  • #include 문은 80문자를 넘을 수 있다.
  • 헤더 보호문은 최대 길이를 넘을 수 있다.

비아스키(non-ascii) 문자

비아스키 문자는 사용하지 않으며 UTF-8 형식을 사용한다.

빈 칸과 탭

탭은 사용하지 않으며 빈 칸만 사용하고 한 번에 4칸을 들여 쓴다.

함수 선언과 정의

반환 타입은 함수 이름과 같은 줄에 쓰고 길이가 맞으면 매개 변수도 그렇게 한다.

한 줄에 모두 쓰지 못하면 다음처럼 작성해 본체와 쉽게 구분할 수 있게 한다.

주의할 몇 가지는 다음과 같다.

  • 매개 변수 이름을 잘 선택한다.
  • 매개 변수를 사용하지 않으며 목적이 명확하면 이름을 생략할 수 있다.
  • 반환 타입과 함수 이름을 한 줄에 둘 수 없으면 둘 사이에서 나눈다.
  • 함수 선언이나 정의의 반환 타입 다음에서 나눌 때는 들여 쓰지 않는다.
  • 여는 괄호는 항상 함수 이름과 같은 줄에 둔다.
  • 함수 이름과 여는 괄호 사이는 띄우지 않는다.
  • 괄호와 매개 변수 사이는 띄우지 않는다.
  • 여는 중괄호는 해당 함수 선언의 다음 줄에서 시작한다.
  • 닫는 중괄호는 여는 중괄호와 같은 줄에 또는 함수 본체 마지막 줄에 둔다.

사용하지 않는 매개 변수 의미가 문맥상 명확하면 생략할 수 있다.

사용하지 않으나 매개 변수 의미를 명확히 표현하고 싶으면 함수 정의에서 해당 변수 이름을 주석 처리한다.

람다 표현식

매개 변수와 본체는 다른 함수와, 갈무리(capture) 목록은 쉼표로 구분한 목록과 형식이 같다. 참조로 갈무리할 때는 앰퍼샌드(&)와 변수 이름 사이를 띄우지 않는다.

짧은 람다는 함수 인자로 인라인할 수 있다.

함수 호출

호출문 전체를 한 줄로 작성하거나, 인자를 줄바꿈 후 4 칸 들여 쓰기한 다음 작성한다. 여는 괄호와 닫는 괄호 앞에는 빈 칸을 두지 않는다. 호출 형식은 다음과 같다.

호출문 전체를 한 줄로 작성할 수 없으면 인자를 줄바꿈 후 4 칸 들여 쓴다.

줄바꿈 후에도 모든 인자를 한 줄에 쓸 수 없으면 4 칸 들여 쓰기하고 인자를 한 줄에 하나씩 쓴다.

긴 표현식

긴 표현식은 연산자 앞에서 줄바꿈 후 4 칸 들여 쓴다.

조건문

괄호 안에는 빈 칸을 두지 않고 ifelse는 서로 다른 줄에 둔다.

if와 여는 괄호 사이, 닫는 괄호와 여는 중괄호 사이는 반드시 띄운다.

짧은 조건문이라도 한 줄에 작성하지 않는다. 조건 이후 단일 문장이 올 때는 중괄호가 필수는 아니나 사용해 가독성을 높일 수 있다.

if-else 문에서 중괄호는 ifelse 모두에 사용하거나 사용하지 않아야 하며 한 쪽만 사용해서는 안 된다.

루프와 switch 문

switch 문 각 구역에 중괄호를 사용할 수 있다. 단일 문장 루프에서 중괄호는 선택적이다. 루프 본체가 비어 있을 때는 {}continue를 사용한다.

switch 문 내 각 case 구역에는 중괄호를 쓰거나 그러지 않을 수 있다. 사용한다면 아래처럼 한다. 열거 값에 해당하지 않는 조건이 있을 때는 항상 default를 둬야 한다. 절대 default 사례를 실행하지 않아야 한다면 assert를 사용한다.

단일 문장 루프에서 중괄호는 선택적이다.

본체가 비어 있으면 {}continue를 사용하고 세미콜론 하나만 사용하지 않는다.

포인터와 참조 표현식

마침표나 화살표 주위에는 빈 칸을 두지 않는다. 포인터 연산자 다음에는 빈 칸을 두지 않는다.

다음을 주의한다.

  • 멤버에 접근할 때 마침표나 화살표 연산자 주위에 빈 칸을 두지 않는다.
  • 포인터 연산자에서 *& 다음에 빈 칸을 두지 않는다.

포인터/참조 변수나 인자를 선언할 때는 연산자를 타입에 붙여 쓴다.

부울 표현식

부울 표현식이 표준 줄 길이보다 더 길면 연산자를 줄 시작 부분에 두도록 나눈다. 괄호를 적절히 사용해 가독성을 높인다.

반환값

return 문을 불필요하게 괄호로 감싸지 않는다.

전저리기 지시자

전처리기 지시자를 시작하는 해시 표시는 항상 줄 처음에 둔다. 전처리기 지시자가 들여 쓰기한 코드 본체 안에 있더라도 지시자는 줄 처음에서 시작한다.

클래스 형식

public, protected, private 순으로 하며 선언 기본 형식은 다음과 같다.

다음을 주의한다.

  • 모든 기초 클래스 이름은 하위 클래스 이름과 같은 줄에 두며 80 문자 제한을 따른다.
  • public, protected, private 키워드는 들여 쓰지 않는다.
  • 이 키워드 앞에서는 한 줄 띄운다. 단 처음 사용한 것에는 적용하지 않는다.
  • 이 키워드 다음에서는 띄우지 않는다.
  • public 구역을 처음으로 하고 protected, private 구역 순으로 한다.
  • 각 구역 안에서 선언 순서는 선언 순서 규칙을 참고한다.

생성자 초기화 목록

생성자 초기화 목록은 모두 한 줄에 쓰거나 다음 줄에 4 칸 들여 쓴다.

네임스페이스 형식

네임스페이스 안 내용은 들여 쓰기 한다.

중첩 네임스페이스를 선언할 때도 들여 쓴다.

수평 빈 칸

일반

줄 끝에는 공백 문자를 두지 않으며 이미 있으면 제거한다.

루프와 조건

연산자

템플릿과 변환

빈 줄

필요 없이 빈 줄을 사용하지 않는다. 특히 함수 사이에는 한 줄이나 두 줄만 띄운다. 함수를 빈 줄로 시작하거나 빈 줄로 마치지 않으며 함수 안에서는 잘 판단해 사용한다. 기본 원칙은 코드를 한 화면 안에 나오게 하면 프로그램 제어 흐름을 따라가고 이해하기 더 쉽다는 것이다. 물론 너무 밀집하거나 퍼뜨려도 가독성이 나빠질 수 있으므로 잘 판단해야 한다. 하지만 일반적으로 빈 줄은 최소로 사용한다.

형식 설정 파일

Visual Studio와 JetBrains IDE용 형식 설정 파일은 다음에서 찾을 수 있다.

 

You may also like...