C++를 위한 CLAUDE.md: AI가 안전하고 현대적이며 관용적인 C++를 작성하게 만드는 13가지 규칙
요약
본 문서는 AI가 생성하는 C++ 코드가 안전하고, 현대적이며, 관용적인 방식으로 작성되도록 돕는 13가지 핵심 규칙을 제시합니다. 이 규칙들은 단순히 최신 기능을 사용하도록 지시하는 것을 넘어, 소유권 관리(스마트 포인터), 리소스 정리(RAII), 그리고 정의되지 않은 동작(UB) 방지 등 프로덕션 환경에서 필수적인 코딩 패턴을 AI에게 명확히 요구함으로써 코드의 안정성을 극대화하는 데 중점을 둡니다.
핵심 포인트
- C++20 또는 C++23과 같은 최신 표준만을 사용하도록 강제하여 레거시 관용구(idioms) 사용을 방지해야 합니다.
- 메모리 소유권은 반드시 `std::unique_ptr`나 `std::shared_ptr`와 같은 스마트 포인터를 사용하고, 로우 포인터는 오직 관찰자(observer) 목적으로만 제한해야 합니다.
- 파일 핸들이나 락(lock) 등 모든 시스템 리소스에는 RAII(Resource Acquisition Is Initialization) 패턴을 적용하여 수동 정리로 인한 누수를 원천 차단해야 합니다.
- 부호 있는 정수 오버플로, 범위를 벗어난 접근, 초기화되지 않은 변수 읽기 등 정의되지 않은 동작(Undefined Behavior, UB)을 절대 허용해서는 안 됩니다.
C++를 위한 CLAUDE.md: AI가 안전하고 현대적이며 관용적인 C++를 작성하게 만드는 13가지 규칙
C++는 AI의 지원이 수천 시간을 절약해 줄 수도 있지만, 프로덕션 환경에서 몇 달 동안 숨어 있는 버그를 유발할 수도 있는 언어입니다. 그 차이는 당신이 AI 어시스턴트에게 어떤 C++를 작성하고 있는지 말했는지 여부에 달려 있습니다. CLAUDE.md가 없다면, 모델은 C++20 코드베이스에 C++98을 작성하거나, 스마트 포인터 (smart pointers)를 기대하는 곳에 로우 포인터 (raw pointers)를 사용하거나, 잘못된 CPU 아키텍처가 드러날 때까지 모든 테스트를 통과하는 미정의 동작 (undefined behavior)을 생성할 수도 있습니다. 이 13가지 규칙은 실제 프로젝트에서 도출되었습니다. AI가 생성한 C++를 안전하고, 현대적이며, 관용적 (idiomatic)으로 유지하는 데 가장 중요한 패턴들입니다.
규칙 1: 표준 고정 — C++20 또는 C++23만 사용
표준: 최소 C++20. 사용 가능한 경우 C++23 기능 사용. 활성화: concepts, ranges, coroutines, std::span, std::format. 금지: 현대적인 대응 방식이 존재할 때 C++98 / 11 / 14 관용구 (idioms) 사용.
AI 모델은 엄청난 양의 레거시 (legacy) C++ 코드를 학습했습니다. 이 규칙이 없다면 std::bind와 람다 (lambdas)가 섞이고, std::format 옆에 printf가 등장하며, ranges를 사용하면 더 깔끔할 곳에 이터레이터 (iterators)가 나타날 것입니다.
중요한 이유: C++20의 concepts만으로도 템플릿 (template) 에러 메시지의 전체 클래스를 제거하고 의도를 명확하게 만들 수 있습니다. std::span은 포인터+크기 (pointer+size) 안티 패턴 (anti-pattern)을 대체합니다. 표준 고정은 모델이 가장 낮은 공통 분모로 기본 설정되는 것을 방지합니다.
// 규칙이 없을 때 — AI가 다음과 같은 코드를 생성할 수 있음
template < typename T >
void process ( T * data , size_t n )
{
...
}
// 규칙이 있을 때 — AI가 다음과 같은 코드를 생성함
template < std :: ranges :: range R >
void process ( R && range )
{
...
}
규칙 2: 스마트 포인터만 사용 — 소유권을 가진 로우 포인터 금지
메모리: 소유권을 위해 std::unique_ptr / std::shared_ptr 사용. 로우 포인터 (Raw pointers): 관찰자 포인터 (observer pointers)로만 사용 (절대 소유하지 않음). 금지: 애플리케이션 코드 내 new / delete 사용. malloc / free는 절대 사용 금지. std::make_unique와 std::make_shared를 독점적으로 사용.
가장 흔한 AI 생성 C++ 실수는 소유권 모델 (ownership models)을 혼용하는 것입니다.
"내가 이것을 소유한다"는 의미임에도 T*를 받는 함수는 메모리 누수(memory leak)가 발생하기를 기다리는 것과 같습니다. 그리고 AI는 명시적인 지시가 없으면 끊임없이 이 패턴을 생성합니다.
// 금지됨 — 소유권을 가진 raw pointer
Widget * createWidget () {
return new Widget (); // 이것을 누가 삭제하나요?
}
// 필수 — 소유권 명시
std::unique_ptr<Widget> createWidget () {
return std::make_unique<Widget>();
}
관찰자 포인터(observer pointer) 예외: T*와 T&는 소유하지 않는 참조(non-owning references)로서 괜찮습니다. 이는 "이것을 빌려 쓰는 것이지, 소유하는 것이 아니다"라는 의미를 전달합니다.
규칙 3: 모든 리소스에 대한 RAII 적용 — 수동 정리 금지
리소스: 모든 리소스를 RAII 타입으로 감싸세요.
- 파일 핸들 (File handles) → std::fstream
- 잠금 (Locks) → std::lock_guard / std::unique_lock
- 네트워크 / OS 핸들 (Network / OS handles) → 소멸자(destructor)가 있는 커스텀 RAII 래퍼
- 리소스를 해제하는 소멸자 없이는 리소스를 획득하지 마세요.
AI는 함수의 끝에 fclose(file)를 생성할 것입니다. 하지만 모든 조기 반환(early return) 지점에서 이를 생성하지는 않을 것입니다. RAII는 이 문제 전체를 제거합니다.
// 규칙이 없는 AI — 조기 반환 시 누수 발생
FILE * f = fopen ("data.bin", "rb");
if (!validate()) return; // 누수 발생
process(f);
fclose(f);
// 규칙 적용 — RAII, 항상 닫힘
{
std::ifstream f ("data.bin", std::ios::binary);
if (!validate()) return; // 소멸자 실행
process(f);
}
규칙 4: 정의되지 않은 동작 (Undefined Behavior, UB) 금지 — 절대 금지
UB 규칙:
- 부호 있는 정수 오버플로 (signed integer overflow) 금지. 오버플로가 예상되는 경우 체크된 산술 연산(checked arithmetic)을 사용하거나 부호 없는 정수(unsigned)를 사용하세요.
- 범위를 벗어난 접근 (out-of-bounds access) 금지. 성능이 중요하지 않은 경로(non-hot paths)에서는 .at()을 사용하세요. 성능이 중요한 경로(hot paths)에서는 경계값(bounds)을 Assert 하세요.
- 초기화되지 않은 읽기 (uninitialized reads) 금지. 모든 변수는 선언 시 초기화하세요.
- 허공에 뜬 참조 (dangling references) 금지. 지역 변수에 대한 참조를 반환하지 마세요.
- 엄격한 에일리어싱 위반 (strict aliasing violations) 금지. reinterpret_cast 대신 std::bit_cast를 사용하세요.
이것은 AI에게 가장 명시적으로 필요한 규칙입니다. x86에서 "작동하는" UB가 ARM에서는 충돌을 일으킬 수 있습니다. 테스트를 통과하는 UB가 공격적인 최적화를 수행하는 컴파일러에 의해 악용될 수 있습니다. 모델들은 컴파일이 된다는 이유만으로 UB를 생성할 것입니다.
// UB (Undefined Behavior) — 초기화되지 않은 변수
int result ;
if ( condition )
result = compute ();
return result ;
// !condition 일 경우 정의되지 않음 (undefined)
// 필수 사항 — 항상 초기화할 것
int result = 0 ;
if ( condition )
result = compute ();
return result ;
Rule 5: 값 의미론 (Value semantics) 선호 — 이동 (move)은 비용이 들지 않음
값 의미론 (Value semantics): 이동 (moving) 비용이 저렴할 때는 값에 의한 전달 (pass by value)을 사용하세요.
값에 의한 반환 (Return by value) — NRVO / RVO 덕분에 이는 비용이 들지 않습니다.
소유권 이전 (transferring ownership) 시에만 명시적으로 std::move를 사용하세요.
읽기 전용인 큰 객체에 대해서는 const 참조 (Const references)를 사용하세요.
출력 매개변수 (output parameters)를 피하세요 — 대신 구조체 (structs)나 std::tuple을 반환하세요.
AI는 기본적으로 C 스타일의 출력 매개변수와 참조가 많은 시그니처 (reference-heavy signatures)를 사용합니다.
이동 의미론 (move semantics)을 사용하는 현대적 C++는 값 의미론 (value semantics)을 빠르고 깔끔하게 만듭니다.
// 규칙이 없는 AI — 출력 매개변수 스타일
void getResult ( std :: vector < int >& out ) {
out = compute ();
}
// 규칙 적용 — 값 의미론 (value semantics)
std :: vector < int > getResult () {
return compute (); // NRVO, 제로 카피 (zero copy)
}
Rule 6: 모든 템플릿 제약 조건에 컨셉 (Concepts) 사용
템플릿: 컨셉 (concepts)으로 제약 조건을 설정하되, 제약이 없는 상태로 두지 마세요.
표준 컨셉을 사용하세요: std::integral, std::floating_point, std::ranges::range, std::invocable.
프로젝트 컨셉은 concepts.hpp 헤더에 정의하세요.
제약 조건 없이 typename T를 사용하지 마세요.
제약이 없는 템플릿은 50줄에 달하는 에러 메시지를 생성합니다. 컨셉 (Concepts)은 호출 지점에서 하나의 명확한 진단 (diagnostic)을 제공합니다.
// 규칙이 없는 경우 — 제약이 없는 템플릿
template < typename T , typename Func >
auto transform ( T container , Func f ) {
...
}
// 규칙 적용 — 의도를 제약하는 컨셉 (concepts)
template < std :: ranges :: input_range R , std :: invocable < std :: ranges :: range_value_t < R > > F >
auto transform ( R && range , F && f ) {
...
}
Rule 7: std::expected 또는 예외 (exceptions)를 이용한 에러 처리 — 에러 코드 사용 금지
에러 처리 (Error handling):
-
실패할 수 있는 함수: std::expected<T, Error> (C++ 23)를 반환하거나 타입이 지정된 예외 (typed exceptions)를 던지세요 (throw).
-
int 반환 코드 사용 금지. errno 사용 금지. 에러를 위한 출력 매개변수 사용 금지.
-
예외 유형 (Exception types): std::runtime_error 또는 std::logic_error로부터 상속받으세요.
-
에러 유형 (Error type): 프로젝트 전용 Error enum 또는 variant를 정의하세요.
-
다시 던지거나 (rethrow) 로깅하지 않는 catch (...) 사용 금지.
AI는 학습 데이터에 포함된 방대한 양의 C 레거시 코드 때문에 에러 코드를 생성합니다. C++는 타입이 지정된 에러 핸들링 (typed error handling)을 누릴 자격이 있습니다.
// 금지 (Banned) — C 스타일 에러 코드
int readFile ( const std::string & path , std::string & out );
// 권장 (Required) — 타입이 지정된 결과
std::expected < std::string , FileError > readFile ( const std::filesystem::path & path );
규칙 8: const가 가능한 모든 곳에 const를 사용하세요
const 규칙:
- 변경되지 않는 모든 변수: const.
- 상태를 변경하지 않는 모든 멤버 함수: const.
- 수정하지 않는 모든 매개변수: const ref.
- 컴파일 타임 상수에는 const보다 constexpr를 선호하세요.
- 반드시 컴파일 타임에 계산되어야 하는 함수에는 consteval을 사용하세요.
const는 의도를 전달하고, 최적화를 가능하게 하며, 특정 부류의 버그를 방지합니다. 당신이 강조하지 않으면 AI는 이를 잊어버립니다.
// 규칙 미적용 — const 기회를 놓침
std::string getName ( User user ) { std::string result = user.name ; return result ; }
// 규칙 적용 — const-correct (const 적정성 준수)
std::string getName ( const User & user ) { const std::string result = user.name ; return result ; }
규칙 9: std::span 및 std::string_view 사용 — raw array나 const char* 사용 금지
버퍼 (Buffers): 연속된 범위(contiguous ranges)를 위해 std::span<T>를 사용하세요. T* + size 형태는 절대 사용하지 마세요.
문자열 (Strings): 읽기 전용 문자열 매개변수를 위해 std::string_view를 사용하세요. const char는 절대 사용하지 마세요.
소유권 문자열 (Ownership strings): std::string을 사용하세요. char[]는 절대 사용하지 마세요.
C 문자열 함수 사용 금지: strlen, strcpy, sprintf는 금지됩니다.
std::span과 std::string_view는 의도를 정확하게 표현하는 제로 코스트 추상화 (zero-cost abstractions)입니다. const char 매개변수는 수명(lifetime)이나 경계(bounds)를 전달할 수 없습니다.
// 금지됨 — raw C-style void process ( const char * data , size_t len );
// 필수 — 표현력이 풍부하고 안전한 void process ( std :: span < const std :: byte > data );
void log ( std :: string_view message );
Rule 10: 구조 분해 할당 (Structured bindings) 및 패턴 매칭 (Pattern matching) — 이를 사용하십시오
구조 분해 할당 (Structured bindings): tuple / pair / struct 분해에 사용하십시오.
std :: variant에 대해 overloaded lambdas를 사용하는 std :: visit를 사용하십시오.
템플릿에서의 컴파일 타임 분기 (compile-time branching)를 위해 if constexpr를 사용하십시오.
인덱스 기반의 tuple 접근 ( std :: get < 0 > )을 피하십시오. 이는 AI가 생성한 C++를 읽기 쉽게 만듭니다.
인덱스 기반 접근과 수동적인 pair.first/pair.second는 노이즈입니다.
// 규칙이 없을 때 — 인덱스 접근
auto result = getCoordinates ();
double x = std :: get < 0 > ( result );
double y = std :: get < 1 > ( result );
// 규칙이 있을 때 — 구조 분해 할당 (Structured binding)
auto [ x , y ] = getCoordinates ();
Rule 11: 스레드 안전성 (Thread safety)은 명시적이어야 합니다 — 암시적 공유를 금지합니다
동시성 (Concurrency):
- 공유 가능한 가변 상태 (Shared mutable state): 항상 std :: mutex로 보호합니다.
- 단일 값 공유 상태에는 std :: atomic을 선호합니다.
- 전역 가변 상태 (Global mutable state)를 금지합니다. 멀티스레드 코드 내의 정적 가변 지역 변수 (static mutable locals)를 금지합니다.
- std :: thread 대신 std :: jthread를 사용합니다 (자동 조인 (automatic join)).
- 수동 조건 변수 (condition variables) 대신 std :: latch / std :: barrier / std :: counting_semaphore를 사용합니다.
AI는 레이스 컨디션 (races)이 포함된 코드를 생성하는데, 그 이유는 레이스 컨디션이 첫 번째 테스트 실행 시에는 크래시를 일으키지 않기 때문입니다. 설정에서 스레드 안전성을 명시적으로 만드는 것은 리뷰 전에 이를 잡아냅니다.
// 규칙이 없을 때 — 조용한 데이터 레이스 (silent data race)
static int counter = 0 ;
void increment () { ++ counter ; }
// 규칙이 있을 때 — 명시적 원자성 (explicit atomic)
static std :: atomic < int > counter { 0 };
void increment () { counter . fetch_add ( 1 , std :: memory_order_relaxed ); }
Rule 12: 경고를 에러로 처리하여 빌드하십시오 — 컴파일러가 리뷰어입니다
빌드 플래그 (필수):
-Wall
-Wextra
-Wpedantic
-Werror
-Wconversion
-Wshadow
-Wnull-dereference
디버그 시 Sanitizers:
-fsanitize = address,undefined
clang-tidy: modernize-* , cppcoreguidelines-* 체크를 활성화합니다.
컴파일이 되는 AI 생성 코드가 완료된 것은 아닙니다.
CLAUDE.md에 제약 조건을 명시적으로 기술하면, Warnings-as-errors (경고를 에러로 처리) 설정은 모델이 더 깨끗한 코드를 생성하도록 강제합니다.
규칙 13: 모듈 (Modules) 또는 네임스페이스 (Namespaces) — 전역 스코프 (global scope)를 절대 오염시키지 마세요.
네임스페이스 (Namespaces): 프로젝트 네임스페이스 내의 모든 심볼 (symbol). 헤더 파일에서 using namespace std; 사용 금지.
모듈 (Modules, C++20): 툴체인 (toolchain)이 지원하는 경우 #include보다 선호.
익명 네임스페이스 (Anonymous namespaces): 번역 단위 (translation-unit) 로컬 심볼용.
상수를 위해 #define 사용 금지 — constexpr을 사용하세요.
헤더에서 using namespace X 사용 금지 — 절대로 사용하지 마세요.
AI는 모든 튜토리얼에 등장하기 때문에 using namespace std;를 사용하는 것을 좋아합니다. 하지만 프로덕션 (production) 헤더에서는 금지됩니다.
// 헤더에서 금지됨
using namespace std;
// 필수 사항
namespace myproject {
constexpr std::size_t kMaxBufferSize = 4096;
class DataProcessor {
std::vector<std::byte> buffer_;
public:
std::expected<void, ProcessError> process(std::span<const std::byte> input);
};
}
C++를 위한 CLAUDE.md 블록
C++ 표준 (C++ Standards)
표준 (Standard): 최소 C++20 (사용 가능한 경우 C++23)
컴파일러 (Compiler): Clang 16+ 또는 GCC 13+
빌드 (Build):
- -Wall
- -Wextra
- -Wpedantic
- -Werror
- -Wconversion
- -Wshadow
메모리 (Memory)
- 소유권 (ownership)을 위해
std::unique_ptr/std::shared_ptr사용.std::make_unique/std::make_shared만 사용. - 원시 포인터 (Raw pointers) = 소유하지 않는 관찰자 (non-owning observer) 용도로만 사용. 애플리케이션 코드에서
new/delete금지. - 모든 리소스(file handles, locks, OS handles)에 대해 RAII 적용.
타입 및 API (Types and APIs)
- 버퍼를 위해
std::span<T>사용 (절대T* + size를 사용하지 말 것). - 읽기 전용 문자열을 위해
std::string_view사용 (절대const char*를 사용하지 말 것). - 실패 가능한 연산 (fallible operations)을 위해
std::expected<T, E>사용 (C++23). 그 외에는 타입화된 예외 (Typed exceptions) 사용. - C 문자열 함수 사용 금지:
strlen/strcpy/sprintf금지.
템플릿 (Templates)
- 모든 템플릿 파라미터 (template parameter)를 컨셉 (concepts)으로 제약하십시오. 단순한
typename T사용 금지. - 인덱스 접근보다 구조적 바인딩 (Structured bindings) 선호. SFINAE보다
if constexpr선호.
안전성 (Safety)
- 미정의 동작 (UB, Undefined Behavior) 방지: 모든 변수를 초기화하십시오. 핫 패스 (hot paths) 외부에서는
.at()을 사용하십시오. 부호 있는 오버플로 (signed overflow) 금지. - 가능한 모든 곳에
const를 사용하십시오. 컴파일 타임 상수에는constexpr을 사용하십시오. - 헤더에서
using namespace사용 금지. 모든 심볼은 프로젝트 네임스페이스 내에 있어야 합니다.
동시성 (Concurrency)
- 공유 가능한 가변 상태 (shared mutable state)에는
std::mutex를 사용하십시오. 단일 값에는std::atomic을 사용하십시오. std::thread대신std::jthread를 사용하십시오.- 전역 가변 상태 (global mutable state)를 사용하지 마십시오.
이것이 중요한 이유
위의 13가지 규칙은 단순한 스타일 선호도를 설명하는 것이 아닙니다. 이는 시니어 엔지니어가 배포할 만한 C++와 코드 리뷰에서 지적받을 C++ 사이의 경계, 혹은 더 나아가 배포된 후 산티자이저 (sanitizers)에서 오류가 발생할 C++ 사이의 경계를 설명합니다. AI 모델들은 수십 년간의 C++ 코드를 학습했으며, 그 대부분은 C++11 이전의 코드입니다. 명시적인 제약 조건이 없다면, 모델들은 학습 데이터 분포의 평균값으로 회귀할 것입니다. 즉, 원시 포인터 (raw pointers), 에러 코드 (error codes), 제약 없는 템플릿 (unconstrained templates), 그리고 플랫폼 특정적 가정 (platform-specific assumptions)을 기본값으로 사용하게 됩니다. 위의 CLAUDE.md 블록은 이러한 기본값을 변화시킵니다. 이것이 코드 리뷰를 없애주는 것은 아니지만, AI가 생성한 C++가 리뷰할 가치가 있는 수준에서 시작할 수 있도록 최소한의 기준 (floor)을 높여줍니다. 이 설정을 한 번 작성하는 데 드는 비용은 약 15분입니다. 하지만 이 설정 없이 AI가 생성한 C++를 리뷰하는 데 드는 비용은 매 스프린트 (sprint)마다 발생합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기