C++ 코딩 표준 (C++ 코어 가이드라인)은(는) 무엇을 하나요?
C++ 코어 가이드라인에서 파생된 현대 C++(C++17/20/23)용 포괄적 코딩 표준입니다. 타입 안전성, 리소스 안전성, 불변성, 명확성을 강제합니다.
사용 시기
- 새 C++ 코드 작성(클래스, 함수, 템플릿)
- 기존 C++ 코드 검토 또는 리팩터링
- C++ 프로젝트에서 아키텍처 결정
- C++ 코드베이스 전반에 일관된 스타일 강제
- 언어 기능 선택(예:
enum vs enum class, 원시 포인터 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 | 원시 포인터나 참조로 소유권을 절대 전송하지 마세요 |
| 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 | 기본 연산을 정의하지 않아도 되면 그러지 마세요 (0의 규칙) |
| C.21 | 복사/이동/소멸자를 정의하거나 =delete하면 모두 처리하세요 (5의 규칙) |
| C.35 | 기반 클래스 소멸자: public virtual 또는 protected non-virtual |
| C.41 | 생성자는 완전히 초기화된 객체를 생성해야 함 |
| C.46 | 단일 인자 생성자는 explicit으로 선언 |
| C.67 | 다형성 클래스는 public 복사/이동을 억제해야 함 |
| C.128 | 가상 함수: virtual, override, final 중 정확히 하나만 지정 |
0의 규칙
struct Employee {
std::string name;
std::string department;
int id;
};
5의 규칙
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 | 원시 포인터(T*)는 소유하지 않음 |
| R.5 | 범위 객체 선호; 불필요하게 힙 할당하지 마세요 |
| R.10 | malloc()/free() 피하기 |
| R.11 | 명시적으로 new와 delete 호출 피하기 |
| R.20 | 소유권을 나타내려면 unique_ptr 또는 shared_ptr 사용 |
| R.21 | 소유권을 공유하지 않는 한 shared_ptr보다 unique_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::lock_guard<std::mutex> lock(mutex_);
queue_.push(value);
}
int pop() {
std::lock_guard<std::mutex> lock(mutex_);
if (queue_.empty()) throw std::runtime_error("queue is empty");
int value = queue_.front();
queue_.pop();
return value;
}
private:
std::queue<int> queue_;
mutable std::mutex mutex_;
};
안티 패턴
- 락 없이 공유 데이터에 접근 (CP.2)
- 락 걸기 전에 알 수 없는 사용자 코드 호출 (CP.22)
- 조건 변수 없이 스핀 대기 (CP.42)
std::lock() 대신 여러 중첩된 lock() 호출 사용
- 락-프리 기법을 간단한 뮤텍스보다 먼저 고려