안녕하세요. 오늘은 제가 C++ 프로그래밍할 때 사용하는 저만의 ASSERT 함수(매크로)에 대해 소개해보겠습니다.
Assertion은 코드의 버그, 오류를 처리하는 방식 중 하나인데요. 코드에서 "변수, 프로그램의 상태가 이러면 안된다." 라고 가정하는 코드입니다. 다른 방식에는 exception, logging, crush 등이 있는데, 저는 코딩 중에 가장 자주 사용하고 선호하는게 Assertion입니다. 물론 다른 것들도 같이 사용하는게 가장 좋습니다(exception 빼고...).
C++는 <cassert>헤더에 assert 함수가 있습니다. 다만 assert조건에 걸리면 assert함수가 호출돼고 브레이크 포인트가 잡힙니다. 하지만 *함수*이다 보니 call stack이 더러워지고 오류 창도 뜹니다.. 또 콘솔 메세지 출력도 제 마음대로 안되고, 코드에 설명을 쓰고 싶으면 assert(exp && "Error") 이런 식으로 써야합니다.


그래서 저는 제가 직접 만들어서 쓰는데요. 제가 사용하는 ASSERT 매크로를 소개해보겠습니다.

내가 쓰는 ASSERT 매크로
#if defined(_WIN32)
#define COREASSERT_DEBUG_BREAK() __debugbreak()
#elif defined(__GNUC__) || defined(__clang__)
#define COREASSERT_DEBUG_BREAK() __builtin_trap()
#else
#define COREASSERT_DEBUG_BREAK() std::abort()
#endif
#ifndef NDEBUG
#define COREASSERT_IMPL(expr, msg) \
do { \
if (!(expr)) { \
std::fprintf(stderr, \
"[ASSERT] %s:%d in %s\n expr: %s\n msg : %s\n", \
__FILE__, __LINE__, __func__, #expr, (msg)); \
std::fflush(stderr); \
COREASSERT_DEBUG_BREAK(); \
} \
} while (0)
#define COREASSERT_NO_MSG(expr) \
do { \
if (!(expr)) { \
std::fprintf(stderr, \
"[ASSERT] %s:%d in %s\n expr: %s\n", \
__FILE__, __LINE__, __func__, #expr); \
std::fflush(stderr); \
COREASSERT_DEBUG_BREAK(); \
} \
} while (0)
#define COREASSERT_GET_MACRO(_1,_2,NAME,...) NAME
#define ASSERT(...) COREASSERT_GET_MACRO(__VA_ARGS__, COREASSERT_IMPL, COREASSERT_NO_MSG)(__VA_ARGS__)
#else
#define ASSERT(...) ((void)0)
#endif
자세한 코드설명은 중요하지 않은거 같고, 주요 기능만 소개하겠습니다.
플랫폼별 브레이크
- Windows: __debugbreak()
- GCC/Clang: __builtin_trap() (일반적으로 SIGILL로 중단)
- 기타: std::abort()
컴파일러, OS와 무관하게 동작하도록 했습니다.
파일/줄/함수 정보 출력
__FILE__, __LINE__, __func__로 정확한 코드 위치와 입력받은 메시지를 남깁니다.
[ASSERT] ass_main.cpp:123 in ass_func
expr: count < capacity
msg : count must be less than capacity.
가변 인자 트릭(__VA_ARGS__)
ASSERT(cond) 과 ASSERT(cond, "message") 모두 지원합니다.
ASSERT(true); // OK!
ASSERT(false, "Hello assert") // OK!
do { ... } while(0) 패턴
단일 문장처럼 동작해 아래같은 구문에서도 안전합니다.
if (state) ASSERT(cond, "OK!!!");
릴리즈 제거
#ifndef NDEBUG ... #else ... #endif 매크로를 사용해 릴리즈모드에선 브레이크 포인트가 사라지고 최적화 됩니다.
주의사항
데이터 상태를 변경하는 코드를 넣지 말기!
릴리즈에서는 ASSERT(...)가 완전히 제거됩니다. 조건식에 있는 코드가 데이터 상태를 변경하면 디버그와 릴리즈의 동작이 달라집니다.
ASSERT(!strcmp(msg, "boguchi")); // OK!
ASSERT(!strcpy(dst, "boguchi")); // Bad! Release에선 strcpy가 호출되지 않음.
스레드/시그널 환경 주의!
fprintf(stderr, ...)는 보통 안전하지만, 시그널 핸들러나 매우 제한된 컨텍스트에서는 사용하지 말것. 전용 ASSERT를 새로 만들거나 로거/크래시 리포터를 사용하는게 안전.
문자열 인자에 콤마 넣지 말기!
매크로 인자 분리 때문에 콤마가 포함된 매크로는 주의. 보통 문자열 리터럴은 문제없지만, FMT(x, y) 같은 매크로 결과를 바로 메시지로 넘기면 인자 분리가 꼬일 수 있습니다. 이럴 땐 괄호로 감싸거나 별도 변수에 담아 넘겨야 합니다. 되도록 리터럴만 넣습니다.
오늘은 제가 사용하는 ASSERT 매크로를 소개해봤습니다. 단순해 보이지만 이런저런 문제들을 겪어보며 계속 수정하면서 사용하다보니,, 꽤 잘 동작합니다. 도움이 됐음 좋겠네요.
감사합니다.