(C/C++) 조건부 컴파일로 디버깅용 출력 한방에 없애기

여러분이 방금 틀렸습니다를 받았던 문제의 코너 케이스를 고쳤다고 해봅시다.

들뜬 마음에 점검도 안하고 바로 백준에 코드를 제출해버립니다!

void AddEdge(int v1, int v2)
{
    adjacencyList[v1].push_back(v2);
    adjacencyList[v2].push_back(v1);
    // 어라? 이거 지웠어야 하는데 그냥 제출해버렸네...
    printf("[DEBUG] Add edge (%d, %d)\n", v1, v2);
}

아... 근데 깜빡하고 디버깅하면서 출력했던 문자열을 안 지우고 제출해서 또 틀렸습니다를 받아버렸습니다...

깜빡하지 않았다고 하더라도, 디버깅용 문자열을 여기저기서 출력했다면 그걸 일일히 찾아서 지우는 것도 꽤 귀찮은 일입니다.

아... 그냥 소스 수정 안하고 제출해도 디버깅용 문자열 출력이 알아서 없어지면 좋을텐데...

바로 그 방법을 소개합니다.


백준 채점기의 `ONLINE_JUDGE` 매크로 이용하기

백준 C/C++ 채점기의 컴파일 옵션을 살펴봅시다.

C++17
언어 번호: 84
컴파일: g++ Main.cc -o Main -O2 -Wall -lm -static -std=gnu++17 -DONLINE_JUDGE -DBOJ
C11
언어 번호: 75
컴파일: gcc Main.c -o Main -O2 -Wall -lm -static -std=gnu11 -DONLINE_JUDGE -DBOJ

-DONLINE_JUDGE -DBOJ 옵션이 들어간 게 보이시나요?

저 두 옵션에 의해 ONLINE_JUDGEBOJ 라는 매크로가 정의되게 됩니다.

저걸 미리 정의된 매크로(Predefined Macros)라고 부릅니다.

쉽게 생각하면 여러분이 백준에 제출한 소스 코드 상단에

#define ONLINE_JUDGE
#define BOJ

가 들어간다고 생각할 수 있죠.

위 두 매크로가 백준 채점기에서만 정의된다는 걸 이용해, 저 매크로가 정의되지 않았을 때에만 디버깅용 출력을 출력하면 됩니다.

전처리기의 조건부 컴파일 기능을 활용하면 쉽게 이걸 구현할 수 있습니다.

void AddEdge(int v1, int v2)
{
    adjacencyList[v1].push_back(v2);
    adjacencyList[v2].push_back(v1);
// 조건부 컴파일: 로컬에서는 컴파일되지만, 백준 채점기에선 지워지고 컴파일된다.
#ifndef ONLINE_JUDGE
    printf("[DEBUG] Add edge (%d, %d)\n", v1, v2);
#endif
}

모든 디버깅 출력 라인을 위와 같이 #ifndef ONLINE_JUDGE ~ #endif 또는 #ifndef BOJ ~ #endif 로 감싸주게 되면, 로컬 환경에선 디버깅 라인이 컴파일이 되고, 백준 채점기 환경에선 디버깅 라인이 삭제되고 컴파일이 됩니다.

즉, 디버깅 출력을 삭제할 것 없이 그대로 제출해도 된다는 말이죠.


사용 예시

제가 실제 제출한 예시입니다. (보시려면 2606번: 바이러스를 푸셨어야 합니다.)

전 이걸 약간 응용해서, ONLINE_JUDGE가 정의되면 NDEBUG가 정의되도록 하고,

#ifdef ONLINE_JUDGE
#define NDEBUG
#endif

모든 디버깅 출력 라인을 #ifndef NDEBUG ~ #endif 로 감쌌습니다.

왜 굳이 NDEBUG 를 한번 거쳐가냐면, cassert 와 assert.h 표준에서 NDEBUG가 정의되면 assert(x) 문장을 무효화하거든요.

저렇게 하면 assert(x)로 디버깅 한것도 백준 채점기에 들어가지 않아서, 실행 시간 이득을 노릴 수 있습니다. (미미하지만.)

(당연히 채점기에서 고의적으로 assert(x) 로 뭔가 체크해 볼 경우에는 위 방법을 쓰면 안되겠죠.)

그리고 main() 함수 제일 윗줄에 다음과 같이 써 줬습니다.

#define FAST_IO std::ios::sync_with_stdio(false); std::cin.tie(nullptr);
// ...
int main()
{
#ifndef NDEBUG
    std::cout << "[DEBUG] Debug enabled!\n";
#else
    FAST_IO;
#endif
    // ...
}

위와 같이 해서 로컬 환경에서 돌릴 땐 FAST_IO(빠른 입출력)이 꺼지게 하고, 제출할 때만 활성화되게 했습니다.

저렇게 하면 디버깅용 라인에 한해서만 printf()std::cout 을 섞어 써도 됩니다.

또한, 로컬 환경에서는 [DEBUG] Debug enabled! 를 띄워 줘서 디버깅이 켜졌다는 걸 알립니다. (이건 사족이긴 한데 나쁠 건 없죠)


debug(x) 한 줄만 써서 조건부 출력

좋아요, 다 좋은데, #ifndef BOJ ~ #endif 로 디버깅 라인을 일일히 감싸는 것도 솔직히 귀찮잖아요?

대부분의 경우 특정 시점의 변수의 값만 출력하면 되는데, 그걸 3줄씩이나 쓰는건 손이 아프단 말이죠.

이 글의 debug(x) 매크로 함수를 cassert 의 조건부 컴파일 방법론과 응용하면, debug(x); 만 써도 x 값 디버깅 출력을 로컬 환경에서만 컴파일 되도록 할 수 있습니다.

#ifdef BOJ
#define debug(x)  ((void)0)
#else
#define debug(x)  std::cout << "[Debug] " << #x << " is " << x << '\n'
#endif

이러면 debug(x); 는 굳이 #ifndef BOJ ~ #endif 로 감싸지 않아도 백준 채점기에선 알아서 출력이 안 되게 됩니다.

백준 채점기에선 무의미한 ((void)0) 으로 변환될테니까요.

이것까지 사용한, 조금 더 복잡한 예시 (이건 16236번: 아기 상어를 풀어야 보입니다.)


응용

이 외에도 조건부 컴파일을 잘 응용하면 좋을 만한 상황이 꽤 많습니다.

대표적으로, 로컬 환경에서 테스트할때는 내가 입력한 숫자랑 출력되는 숫자가 섞여 나와서 구분이 어려운데,

조건부 컴파일을 이용해 로컬 환경에서만 출력문 앞에 [OUTPUT] 을 붙인다던가 하는 게 가능하죠.

뭐 이런 응용은 직접 구현해보시면 좋을 것 같네요.


조건부 컴파일의 일반적인 활용

꼭 백준 문제풀이만이 아니더라도 C/C++ 조건부 컴파일을 잘 알아두면 좋습니다.

대표적으로 크로스 플랫폼 대응하는 코드를 짤 때 WIN32, MACOSUNIX 같은 매크로로 OS-specific 한 코드를 조건부 컴파일 할 수 있겠죠.

제가 아는 어떤 사람은 매크로 하나로 수많은 전역 변수들 앞에 static 을 뗐다 붙였다 하는 식으로 응용하더군요.


댓글 (11개) 댓글 쓰기


adxx 2년 전

어마어마 하네요


copyrat90 2년 전

감사합니다.


kim4969 2년 전

오호 유익합니다. Pre-defined Compiler Macros 입니다.


copyrat90 2년 전

오, 정확한 명칭 알려주셔서 감사합니다. 본문에 추가하겠습니다.


dohoon 2년 전

우와 debug(x) 반갑네요 ㅎㅎ 참고로 #define debug(x) 라고만 적고 뒤에 안 적으면 ((void)0)과 같은 효과를 낼 수 있는 것 같아요.

조건부 컴파일의 일반적인 활용> 저는: on - #define el '\n' off - #define el endl 이렇게 쓰고 있는데, 이 부분의 장점은 여러 테스트 케이스가 들어오는 문제들에 대해서 하나씩 interactive하게 확인할 수 있다는 겁니다.

참고로, 몇몇 judge들은 ONLINE_JUDGE가 없는 경우도 있어서 컴파일 명령어에 -DLOCAL을 넣는 것도 유용할 것 같아요. (앳코더도 아마 바뀌었는지 모르겠지만 안되는 것으로 알고 있어요)

유익한 글 감사합니다! 이런 매크로들 처음에 찾을 때 헤맸는데 필요한 것들이 다 적혀 있어서 너무 좋네요 :)


copyrat90 2년 전

-DLOCAL 좋은 접근 방법이네요. 매크로 없는 저지에서 써먹기 좋겠군요.

뒤에 ((void)0) 를 안 적어도 된다라... 흥미롭네요. 표준 cassert 에선 왜 ((void)0) 를 적었는지 궁금해지는군요. 뭔가 특수한 상황이 있는걸까요?


jry9913 2년 전

제 것도 공유합니다. -DLOCAL을 사용했어요

#include <bits/stdc++.h>

#define debug if constexpr (local) std::cout
#define endl '\n'

#ifdef LOCAL
constexpr bool local = true;
#else
constexpr bool local = false;
#endif

using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);

    if constexpr (local) 
        (void)!freopen("input.txt", "r", stdin);

    debug << "hellow world" << endl;

    return 0;
}

copyrat90 2년 전

와, C++17 의 if constexpr 쓰니 아주 깔끔해지네요. 공유 감사합니다.


copyrat90 2년 전

('22. 02. 09 업뎃) 요즘에는 C++17 의 if constexpr 과 가변인자 매크로(__VA_ARGS__) 응용해서 이렇게 쓰고 있습니다. 답을 출력할 때는 cout 이든, printf 든 하나로만 해야 합니다.

#include <bits/stdc++.h>
using namespace std;

#ifdef ONLINE_JUDGE
constexpr bool ndebug = true;
#else
constexpr bool ndebug = false;
#endif
#define FAST_IO \
    if constexpr (ndebug) { cin.tie(nullptr); ios::sync_with_stdio(false); }
#define debug(x) \
    if constexpr (!ndebug) cout << "[DEBUG] " << #x << " == " << x << '\n';
#define debugf(...) \
    if constexpr (!ndebug) { cout << "[DEBUG] "; printf(__VA_ARGS__); }
#define debugc(c) \
    if constexpr (!ndebug) { cout << "[DEBUG] "<< #c << ": "; for (const auto& elem : c) cout << elem << ", "; cout << '\n'; }

int main()
{
    FAST_IO;
    int foo = 79, bar = 42;
    vector<int> vec = { 4, 5, 7 };
    debug(foo);
    debugf("foo, bar: %d, %d", foo, bar);
    debugc(vec);
}

위 코드는 if constexpr 때문에 C++17 이상에서만 동작하긴 하는데, #ifndef ~ #endif 로 쉽게 고칠 수 있을 겁니다. 본문도 좀 업뎃이 필요해보이긴 하는데, 근본적으로 내용이 달라진 건 아니고, 자잘한 편의성 개선만 있어서 글 고치기가 애매하네요.


euphoric_n 2년 전

이거 보고 나서

#ifndef ONLINE_JUDGE
// 디버깅용 출력
#endif

쓰다가 ONLINE_JUDGE에 오타내서 생고생했네요

다른분들은 조심하세요


junah 10달 전

void dprintf(const char *__format, ...)
{
#ifndef ONLINE_JUDGE
    va_list args;
    va_start(args, __format);
    vprintf(__format, args);
    va_end(args);
#endif
}

dprintf("%d %d", a, b);

위와 같은 방식으로 scanf, printf 쓰시는 분들도 쉽게 활용할 수 있습니다.