C++ 코딩 표준 (C++ Core Guidelines)
C++ Core Guidelines에서 도출된 최신 C++(C++17/20/23)용 포괄적 코딩 표준입니다. 타입 안전성, 리소스 안전성, 불변성 및 명확성을 적용합니다.
사용 시기
- 새 C++ 코드(클래스, 함수, 템플릿) 작성
- 기존 C++ 코드 검토 또는 리팩토링
- C++ 프로젝트의 아키텍처 결정
- C++ 코드베이스에 걸쳐 일관된 스타일 적용
- 언어 기능 선택(예:
enum vs enum class, raw 포인터 vs 스마트 포인터)
사용하지 말아야 할 경우
- C++ 이외의 프로젝트
- 최신 C++ 기능을 도입할 수 없는 레거시 C 코드베이스
- 특정 가이드라인이 하드웨어 제약과 충돌하는 임베디드/베어메탈 환경(선별적으로 적용)
교차 절단 원칙
다음 주제는 전체 가이드라인에 걸쳐 반복되며 기초를 형성합니다:
- 모든 곳에서의 RAII (P.8, R.1, E.6, CP.20): 리소스 수명을 객체 수명에 결합
- 기본 불변성 (P.10, Con.1-5, ES.25):
const/constexpr로 시작, 가변성은 예외
- 타입 안전성 (P.4, I.4, ES.46-49, Enum.3): 타입 시스템을 사용하여 컴파일 타임에 오류 방지
- 의도 표현 (P.3, F.1, NL.1-2, T.10): 이름, 타입 및 개념이 목적을 전달해야 함
- 복잡도 최소화 (F.2-3, ES.5, Per.4-5): 단순한 코드가 올바른 코드
- 포인터 의미론보다 값 의미론 (C.10, R.3-5, F.20, CP.31): 값 반환 및 범위 기반 객체 선호
철학 및 인터페이스 (P., I.)
주요 규칙
| 규칙 | 요약 |
|---|
| P.1 | 아이디어를 코드로 직접 표현하세요 |
| P.3 | 의도를 표현하세요 |
| P.4 | 이상적으로 프로그램은 정적으로 타입 안전해야 합니다 |
| P.5 | 런타임 검사보다 컴파일 타임 검사를 선호하세요 |
| P.8 | 리소스를 누수하지 마세요 |
| P.10 | 가변 데이터보다 불변 데이터를 선호하세요 |
| I.1 | 인터페이스를 명시적으로 만드세요 |
| I.2 | 비-const 전역 변수를 피하세요 |
| I.4 | 인터페이스를 정확하고 강력하게 타입화하세요 |
| I.11 | raw 포인터나 참조로 소유권을 전송하지 마세요 |
| I.23 | 함수 인자 수를 적게 유지하세요 |
권장
struct Temperature {
double kelvin;
};
Temperature boil(const Temperature& water);
금지
double boil(double* temp);
int g_counter = 0;
함수 (F.*)
주요 규칙
| 규칙 | 요약 |
|---|
| F.1 | 의미 있는 연산을 신중하게 이름 지어진 함수로 패키징하세요 |
| F.2 | 함수는 단일 논리 연산을 수행해야 합니다 |
| F.3 | 함수를 짧고 단순하게 유지하세요 |
| F.4 | 함수가 컴파일 타임에 평가될 수 있다면 constexpr로 선언하세요 |
| F.6 | 함수가 예외를 던지지 않아야 한다면 noexcept로 선언하세요 |
| F.8 | 순수 함수를 선호하세요 |
| F.16 | "in" 매개변수의 경우 저렴하게 복사되는 타입은 값으로, 나머지는 const&로 전달하세요 |
| F.20 | "out" 값의 경우 출력 매개변수보다 반환 값을 선호하세요 |
| F.21 | 여러 "out" 값을 반환하려면 구조체 반환을 선호하세요 |
| F.43 | 지역 객체에 대한 포인터나 참조를 절대 반환하지 마세요 |
매개변수 전달
void print(int x);
void analyze(const std::string& data);
void transform(std::string s);
struct ParseResult {
std::string token;
int position;
};
ParseResult parse(std::string_view input);
void parse(std::string_view input,
std::string& token, int& pos);
순수 함수와 constexpr
constexpr int factorial(int n) noexcept {
return (n <= 1) ? 1 : n * factorial(n - 1);
}
static_assert(factorial(5) == 120);
안티 패턴
- 함수에서
T&& 반환 (F.45)
va_arg / C 스타일 가변 인자 사용 (F.55)
- 다른 스레드로 전달되는 람다에서 참조로 캡처 (F.53)
- 이동 의미론을 억제하는
const T 반환 (F.49)
클래스 및 클래스 계층 구조 (C.*)
주요 규칙
| 규칙 | 요약 |
|---|
| C.2 | 불변 조건이 있으면 class를, 데이터 멤버가 독립적으로 변하면 struct를 사용하세요 |
| C.9 | 멤버 노출을 최소화하세요 |
| C.20 | 기본 연산 정의를 피할 수 있다면 피하세요 (Rule of Zero) |
| C.21 | 복사/이동/소멸자 중 하나를 정의하거나 =delete하면 모두 처리하세요 (Rule of Five) |
| C.35 | 기반 클래스 소멸자: public 가상 또는 protected 비가상 |
| C.41 | 생성자는 완전히 초기화된 객체를 생성해야 합니다 |
| C.46 | 단일 인자 생성자는 explicit으로 선언하세요 |
| C.67 | 다형 클래스는 public 복사/이동을 억제해야 합니다 |
| C.128 | 가상 함수: virtual, override, final 중 정확히 하나만 지정하세요 |
Rule of Zero
struct Employee {
std::string name;
std::string department;
int id;
};
Rule of Five
class Buffer {
public:
explicit Buffer(std::size_t size)
: data_(std::make_unique<char[]>(size)), size_(size) {}
~Buffer() = default;
Buffer(const Buffer& other)
: data_(std::make_unique<char[]>(other.size_)), size_(other.size_) {
std::copy_n(other.data_.get(), size_, data_.get());
}
Buffer& operator=(const Buffer& other) {
if (this != &other) {
auto new_data = std::make_unique<char[]>(other.size_);
std::copy_n(other.data_.get(), other.size_, new_data.get());
data_ = std::move(new_data);
size_ = other.size_;
}
return *this;
}
Buffer(Buffer&&) noexcept = default;
Buffer& operator=(Buffer&&) noexcept = default;
private:
std::unique_ptr<char[]> data_;
std::size_t size_;
};
클래스 계층 구조
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
};
class Circle : public Shape {
public:
explicit Circle(double r) : radius_(r) {}
double area() const override { return 3.14159 * radius_ * radius_; }
private:
double radius_;
};
안티 패턴
- 생성자/소멸자에서 가상 함수 호출 (C.82)
- 비-사소한 타입에
memset/memcpy 사용 (C.90)
- 가상 함수와 재정의자에 다른 기본 인자 제공 (C.140)
- 데이터 멤버를
const 또는 참조로 만들어 이동/복사 억제 (C.12)
리소스 관리 (R.*)
주요 규칙
| 규칙 | 요약 |
|---|
| R.1 | RAII를 사용하여 리소스를 자동으로 관리하세요 |
| R.3 | raw 포인터(T*)는 소유권이 없습니다 |
| R.5 | 범위 기반 객체를 선호하세요, 불필요하게 힙 할당하지 마세요 |
| R.10 | malloc()/free()를 피하세요 |
| R.11 | new와 delete를 명시적으로 호출하는 것을 피하세요 |
| R.20 | 소유권을 나타내려면 unique_ptr 또는 shared_ptr을 사용하세요 |
| R.21 | 소유권을 공유하지 않는 한 unique_ptr을 shared_ptr보다 선호하세요 |
| R.22 | shared_ptr을 만들려면 make_shared()를 사용하세요 |
스마트 포인터 사용
auto widget = std::make_unique<Widget>("config");
auto cache = std::make_shared<Cache>(1024);
void render(const Widget* w) {
if (w) w->draw();
}
render(widget.get());
RAII 패턴
class FileHandle {
public:
explicit FileHandle(const std::string& path)
: handle_(std::fopen(path.c_str(), "r")) {
if (!handle_) throw std::runtime_error("Failed to open: " + path);
}
~FileHandle() {
if (handle_) std::fclose(handle_);
}
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
FileHandle(FileHandle&& other) noexcept
: handle_(std::exchange(other.handle_, nullptr)) {}
FileHandle& operator=(FileHandle&& other) noexcept {
if (this != &other) {
if (handle_) std::fclose(handle_);
handle_ = std::exchange(other.handle_, nullptr);
}
return *this;
}
private:
std::FILE* handle_;
};
안티 패턴
- 맨
new/delete (R.11)
- C++ 코드에서
malloc()/free() (R.10)
- 단일 표현식에서 여러 리소스 할당 (R.13 -- 예외 안전성 위험)
unique_ptr로 충분한 곳에 shared_ptr 사용 (R.21)
표현식 및 문장 (ES.*)
주요 규칙
| 규칙 | 요약 |
|---|
| ES.5 | 범위를 작게 유지하세요 |
| ES.20 | 항상 객체를 초기화하세요 |
| ES.23 | {} 초기화 구문을 선호하세요 |
| ES.25 | 수정이 의도되지 않은 경우 객체를 const 또는 constexpr로 선언하세요 |
| ES.28 | const 변수의 복잡한 초기화에는 람다를 사용하세요 |
| ES.45 | 매직 상수를 피하고 기호 상수를 사용하세요 |
| ES.46 | 좁혀지는/손실이 있는 산술 변환을 피하세요 |
| ES.47 | 0이나 NULL 대신 nullptr을 사용하세요 |
| ES.48 | 캐스트를 피하세요 |
| ES.50 | const를 캐스트하여 제거하지 마세요 |
초기화
const int max_retries{3};
const std::string name{"widget"};
const std::vector<int> primes{2, 3, 5, 7, 11};
const auto config = [&] {
Config c;
c.timeout = std::chrono::seconds{30};
c.retries = max_retries;
c.verbose = debug_mode;
return c;
}();
안티 패턴
- 초기화되지 않은 변수 (ES.20)
- 포인터에
0 또는 NULL 사용 (ES.47 -- nullptr 사용)
- C 스타일 캐스트 (ES.48 --
static_cast, const_cast 등 사용)
const를 캐스트하여 제거 (ES.50)
- 이름 있는 상수 없는 매직 넘버 (ES.45)
- 부호 있는/부호 없는 산술 혼합 (ES.100)
- 중첩된 범위에서 이름 재사용 (ES.12)
오류 처리 (E.*)
주요 규칙
| 규칙 | 요약 |
|---|
| E.1 | 설계 초기에 오류 처리 전략을 개발하세요 |
| E.2 | 함수가 할당된 작업을 수행할 수 없음을 알리기 위해 예외를 던지세요 |
| E.6 | RAII를 사용하여 누수를 방지하세요 |
| E.12 | 던지는 것이 불가능하거나 허용되지 않을 때 noexcept를 사용하세요 |
| E.14 | 목적에 맞게 설계된 사용자 정의 타입을 예외로 사용하세요 |
| E.15 | 값으로 던지고, 참조로 잡으세요 |
| E.16 | 소멸자, 해제, swap은 절대 실패해서는 안 됩니다 |
| E.17 | 모든 함수에서 모든 예외를 잡으려고 하지 마세요 |
예외 계층 구조
class AppError : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
class NetworkError : public AppError {
public:
NetworkError(const std::string& msg, int code)
: AppError(msg), status_code(code) {}
int status_code;
};
void fetch_data(const std::string& url) {
throw NetworkError("connection refused", 503);
}
void run() {
try {
fetch_data("https://api.example.com");
} catch (const NetworkError& e) {
log_error(e.what(), e.status_code);
} catch (const AppError& e) {
log_error(e.what());
}
}
안티 패턴
int나 문자열 리터럴 같은 내장 타입 던지기 (E.14)
- 값으로 잡기 (슬라이싱 위험) (E.15)
- 조용히 오류를 삼키는 빈 catch 블록
- 흐름 제어에 예외 사용 (E.3)
errno 같은 전역 상태 기반 오류 처리 (E.28)
상수 및 불변성 (Con.*)
모든 규칙
| 규칙 | 요약 |
|---|
| Con.1 | 기본적으로 객체를 불변으로 만드세요 |
| Con.2 | 기본적으로 멤버 함수를 const로 만드세요 |
| Con.3 | 기본적으로 포인터와 참조를 const로 전달하세요 |
| Con.4 | 생성 후 변경되지 않는 값에 const를 사용하세요 |
| Con.5 | 컴파일 타임에 계산 가능한 값에 constexpr을 사용하세요 |
class Sensor {
public:
explicit Sensor(std::string id) : id_(std::move(id)) {}
const std::string& id() const { return id_; }
double last_reading() const { return reading_; }
void record(double value) { reading_ = value; }
private:
const std::string id_;
double reading_{0.0};
};
void display(const Sensor& s) {
std::cout << s.id() << ": " << s.last_reading() << '\n';
}
constexpr double PI = 3.14159265358979;
constexpr int MAX_SENSORS = 256;
동시성 및 병렬성 (CP.*)
주요 규칙
| 규칙 | 요약 |
|---|
| CP.2 | 데이터 경합을 피하세요 |
| CP.3 | 쓰기 가능한 데이터의 명시적 공유를 최소화하세요 |
| CP.4 | 스레드보다 작업 단위로 생각하세요 |
| CP.8 | 동기화를 위해 volatile을 사용하지 마세요 |
| CP.20 | RAII를 사용하고 절대 lock()/unlock()을 직접 사용하지 마세요 |
| CP.21 | 여러 뮤텍스를 획득하려면 std::scoped_lock을 사용하세요 |
| CP.22 | 락을 보유한 상태에서 알 수 없는 코드를 호출하지 마세요 |
| CP.42 | 조건 없이 기다리지 마세요 |
| CP.44 | lock_guard와 unique_lock의 이름을 기억하세요 |
| CP.100 | 절대적으로 필요하지 않다면 락-프리 프로그래밍을 사용하지 마세요 |
안전한 락킹
class ThreadSafeQueue {
public:
void push(int value) {
std::scoped_lock lock(mutex_);
queue_.push(value);
}
std::optional<int> pop() {
std::scoped_lock lock(mutex_);
if (queue_.empty()) return std::nullopt;
int value = queue_.front();
queue_.pop();
return value;
}
private:
std::queue<int> queue_;
mutable std::mutex mutex_;
};
조건 변수
std::condition_variable cv;
std::mutex cv_mutex;
bool ready{false};
void worker_thread() {
std::unique_lock lock(cv_mutex);
cv.wait(lock, [] { return ready; });
}
void signal_thread() {
{
std::scoped_lock lock(cv_mutex);
ready = true;
}
cv.notify_one();
}
안티 패턴
- 동기화에
volatile 사용 (CP.8)
std::lock_guard 없이 lock()/unlock() 직접 호출 (CP.20)
- 락을 보유한 상태에서 사용자 콜백 호출 (CP.22 -- 데드락 위험)
- 조건 없이
wait() 사용 (CP.42 -- 가짜 깨움)
- 원자적 연산만으로 동시성 문제 해결 시도 (CP.100)
템플릿 및 제네릭 프로그래밍 (T.*)
주요 규칙
| 규칙 | 요약 |
|---|
| T.10 | 모든 템플릿에 대한 개념을 지정하세요 |
| T.20 | "추상"과 "구체적인" 의미론 간의 관계를 개념으로 표현하세요 |
| T.40 | 연산자 오버로딩을 통해 알고리즘 인자를 개념적으로 사용하세요 |
| T.41 | 템플릿에서는 프로토타입 요구 사항만 충분히 설명하세요 |
| T.42 | 더 나은 가독성과 정확성을 위해 템플릿 별칭을 사용하세요 |
| T.43 | using을 사용하여 typedef보다 선호하세요 |
| T.44 | 함수 템플릿을 특수화하는 것보다 오버로딩을 사용하세요 |
| T.46 | 템플릿 인자는 const 참조로 전달하세요 |
| T.47 | 높은 컴파일 시간 비용이 드는 템플릿을 피하세요 |
| T.48 | 컴파일 타임 계산에는 constexpr 템플릿을 사용하세요 |
| T.60 | 인터페이스로서의 특성(traits)보다 템플릿 별칭을 선호하세요 |
| T.65 | 태그 디스패치를 사용하여 대체 구현을 선택하세요 |
| T.120 | 템플릿 메타프로그래밍에는 개념만 사용하세요 |
개념 (C++20)
template <typename T>
concept Hashable = requires(T a) {
{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
template <Hashable T>
void process(const T& value) {
std::size_t hash = std::hash<T>{}(value);
}
template <typename T>
concept Streamable = requires(std::ostream& os, T value) {
{ os << value } -> std::same_as<std::ostream&>;
};
template <Streamable T>
void print(const T& value) {
std::cout << value << '\n';
}
템플릿 메타프로그래밍
template <int N>
struct Factorial {
static constexpr int value = N * Factorial<N - 1>::value;
};
template <>
struct Factorial<0> {
static constexpr int value = 1;
};
static_assert(Factorial<5>::value == 120);
안티 패턴
- 템플릿 인자 제약 부족 (T.10): 의미 없는 오류 메시지 발생
- 오버로딩 대신 템플릿 특수화 사용 (T.44): 예상치 못한 동작 발생 가능
- 고비용 템플릿 컴파일 (T.47): 빌드 시간 증가
- 비-타입 템플릿 인자에 raw 포인터 사용 (T.53): 더 나은 대안 존재
소스 코드 구성 및 네임스페이스 (SF., NL.)
파일 구성
| 규칙 | 요약 |
|---|
| SF.1 | .h / .cpp 파일 쌍을 사용하여 선언과 정의를 분리하세요 |
| SF.2 | .h 파일은 #include를 제외한 다른 것을 포함해서는 안 됩니다 |
| SF.3 | 모든 .h 파일에 #include 가드를 사용하세요 |
| SF.4 | 필요한 경우에만 .h 파일에 인라인 함수를 포함하세요 |
| SF.5 | .cpp 파일은 필요한 헤더만 포함해야 합니다 |
| SF.6 | using namespace를 전역 범위가 아닌 함수/네임스페이스 범위로 제한하세요 |
| SF.7 | 헤더가 자기 충족적이도록 만드세요 (필요한 모든 헤더를 포함) |
| SF.8 | 의존성 그래프를 비순환적으로 유지하세요 |
| SF.21 | 헤더에 익명 네임스페이스를 사용하지 마세요 |
네임스페이스 사용
namespace MyLibrary {
namespace Detail {
void internal_helper();
}
class PublicAPI {
public:
void do_something();
};
}
void example() {
using namespace std::chrono_literals;
auto timeout = 30ms;
}
namespace {
int internal_counter = 0;
void helper_function() {
}
}
명명 규칙
| 규칙 | 요약 |
|---|
| NL.1 | 코드에서 의도를 표현하세요 |
| NL.2 | 주석에서 의도를 기술하세요 |
| NL.5 | 타입 이름은 대문자로 시작하고 함수 이름은 소문자로 시작하세요 |
| NL.7 | 이름을 발음할 수 있고 검색 가능하게 만드세요 |
| NL.9 | 줄당 하나의 선언만 사용하세요 |
| NL.10 | _ 접두사나 접미사를 피하세요 |
| NL.15 | 주석을 적게 사용하되, 사용하는 경우 품질을 유지하세요 |
| NL.16 | 일반적인 코딩 스타일을 사용하세요 |
| NL.17 | 간결한 주석보다 설명적인 이름을 사용하세요 |
| NL.18 | C 스타일 주석보다 C++ 스타일 주석을 사용하세요 |
| NL.19 | 기본적인 명명 규칙을 따르지 않은 이름을 피하세요 |
| NL.20 | 이름 외에는 공백을 사용하지 마세요 |
| NL.25 | 매개변수로 전달된 포인터를 nullptr에 대해 테스트하지 마세요 |
| NL.26 | 일반적인 규칙을 재정의할 때만 주석을 사용하세요 |
| NL.27 | 주석이 아닌 코드를 주석 처리하세요 |
| NL.31 | 일관된 들여쓰기를 사용하세요 |
| NL.35 | 함수, 클래스 및 네임스페이스 주위에 빈 줄을 사용하세요 |
권장 개발 환경 설정
CMake 구성 (C++17 이상)
cmake_minimum_required(VERSION 3.15)
project(MyProject LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) # SF.1: 표준 C++만 사용
# 경고 수준 높이기
if(MSVC)
add_compile_options(/W4 /permissive-)
else()
add_compile_options(-Wall -Wextra -Wpedantic -Werror)
endif()
add_executable(my_app main.cpp)
clang-tidy 구성
Checks: >
clang-*,
cpp-coreguidelines-*,
modernize-*,
performance-*,
readability-*,
-modernize-use-trailing-return-type
WarningsAsErrors: '*'
HeaderFilterRegex: '.*'
참고 자료
라이선스
이 문서는 C++ Core Guidelines를 기반으로 합니다. C++ Core Guidelines는 CC-BY-4.0 라이선스에 따라 배포됩니다.