Macro 분기
vulkan 래핑 작업을 하던 중 로그 출력 및 에러 처리에 대한 필요성이 생겨 어떻게 구현할 지 고민했다.
생각해본 구현 요구사항은 다음과 같다.
1. 간단히 사용할 수 있어야 한다.
2. std library의 의존성을 최소화 해야 한다.
3. 어떤 파일의 몇 번째 줄에서 에러가 발생했는지 출력 할 수 있어야 한다.
4. 에러 혹은 로그의 수준에 따라 출력 포맷을 다르게 할 수 있어야 한다.
5. 에러 코드의 출력 여부를 선택할 수 있어야 한다.
요구사항 2 에 의해 선언부와 정의부를 분리해야 하므로, inline 및 template를 사용할 수 없다.
요구사항 3 에 의해 매크로를 사용해야 한다. (__FILE__, __LINE__ 사용)
- 호출 한 시점의 파일과 줄 번호를 출력해주므로, 매크로 (치환) 가 필요하다.
매크로를 사용하기로 결정했으므로 요구사항 1 을 위해 LOG(...) 와 같이 사용자는 하나의 매크로만으로 사용할 수 있으면 좋을 것이다.
그리고 에러 혹은 로그 수준에 따라 출력 포맷이 달라지고 함수에 전달되는 인자도 다를 것이므로 매크로의 "분기" 가 필요하다.
먼저 알아야 될 내용은 가변 인자 매크로에 대한 내용이다.
가변 인자 매크로는 가변 인자 함수처럼 (...) 을 통해 여러 인자를 받을 수 있고, 받은 인자는 __VA_ARGS__ 로 사용할 수 있다.
#define LOG(...) func(__VA_ARGS__)
void func(...) { ... }
enum {
NONE = 0,
ERROR = 1
};
int main() {
LOG(ERROR);
LOG(ERROR, "TEST");
return 0;
}
이 때 주의할 점은 가변 인자 매크로는 인자가 1개 이상이어야 하고, 가변 인자 (...) 는 매크로 정의 시 마지막에만 위치할 수 있다.
그리고 매크로가 치환된다는 것을 이용하면 매크로 안에 또 다른 매크로를 넣어 사용할 수 있다.
이를 이용하여 매크로 분기를 구현할 것이고, 어떤 다른 매크로의 이름으로 치환해주는 매크로를 추가하는 작업을 한다.
예를 들어 인자가 2개일 때와 3개일 때 다른 함수를 호출하는 매크로를 구현한다고 해보자.
#define LOG_ARGS_2(errorType, message) func_args2(errorType, message);
#define LOG_ARGS_3(errorType, message, errorCode) func_args3(errorType, message, errorCode);
#define LOG_SELECTOR(FUNC_NAME, ...) FUNC_NAME
#define LOG(...) LOG_SELECTOR(__VA_ARGS__)(__VA_ARGS__)
void func_args2(int, const char*) { ... }
void func_args3(int, const char*, int) { ... }
enum {
NONE = 0,
ERROR = 1
};
int main() {
LOG(ERROR, "TEST");
// 치환 순서
// 1. LOG_SELECTOR(1, "TEST")(1, "TEST");
// 2. 1(1, "TEST");
return 0;
}
LOG_SELECTOR 매크로가 다른 매크로의 이름으로 치환해주는 역할을 한다.
위의 코드는 FUNC_NAME이 정수 1로 치환되어 에러가 발생하고,
FUNC_NAME을 LOG_ARGS_2 혹은 LOG_ARGS_3 으로 맞춰주어야 한다.
먼저 생각해볼 수 있는 건 LOG_SELECTOR(__VA_ARGS__) 의 __VA_ARGS__ 앞에 매크로 이름을 추가하는 것인데,
앞에 추가하면 항상 똑같은 매크로만 호출하게 된다는 문제가 생긴다. 따라서, __VA_ARGS__ 의 뒤에 추가해야 한다.
그리고 LOG_SELECTOR 매크로의 인자로 FUNC_NAME 인자 앞에 최대 인자 수 만큼 더미 인자를 넣어야 한다.
- __VA_ARGS__ 가 최대 인자 수 만큼 전달될 수 있기 때문이다.
우리는 최대 3개의 인자까지 지원 할 것이므로 3개의 더미 인자를 추가한다.
#define LOG_ARGS_2(errorType, message) func_args2(errorType, message);
#define LOG_ARGS_3(errorType, message, errorCode) func_args3(errorType, message, errorCode);
#define LOG_SELECTOR(_1, _2, _3, FUNC_NAME, ...) FUNC_NAME
#define LOG(...) LOG_SELECTOR(__VA_ARGS__, LOG_ARGS_2, LOG_ARGS_3)(__VA_ARGS__)
void func_args2(int, const char*) { ... }
void func_args3(int, const char*, int) { ... }
enum {
NONE = 0,
ERROR = 1
};
int main() {
LOG(ERROR, "TEST");
// 치환 순서
// 1. LOG_SELECTOR(1, "TEST", LOG_ARGS_2, LOG_ARGS_3)(1, "TEST");
// 2. LOG_ARGS_3(1, "TEST");
// 3. func_args3(1, "TEST"); // COMPILE ERROR!
LOG(ERROR, "TEST", -1);
// 치환 순서
// 1. LOG_SELECTOR(1, "TEST", -1, LOG_ARGS_2, LOG_ARGS_3)(1, "TEST", -1);
// 2. LOG_ARGS_2(1, "TEST", -1);
// 3. func_args2(1, "TEST", -1); // COMPILE ERROR!
return 0;
}
그런데 치환 순서에서 볼 수 있듯이 두 매크로 이름이 바뀌어 있는 것을 확인할 수 있다.
따라서 LOG_SELECTOR(__VA_ARGS__) 의 뒷 부분에는 매크로 이름을 인자 수의 내림차순으로 나열해야 정상 동작한다.
#define LOG_ARGS_2(errorType, message) func_args2(errorType, message);
#define LOG_ARGS_3(errorType, message, errorCode) func_args3(errorType, message, errorCode);
#define LOG_SELECTOR(_1, _2, _3, FUNC_NAME, ...) FUNC_NAME
#define LOG(...) LOG_SELECTOR(__VA_ARGS__, LOG_ARGS_3, LOG_ARGS_2)(__VA_ARGS__)
void func_args2(int, const char*) { ... }
void func_args3(int, const char*, int) { ... }
enum {
NONE = 0,
ERROR = 1
};
int main() {
LOG(ERROR, "TEST");
// 치환 순서
// 1. LOG_SELECTOR(1, "TEST", LOG_ARGS_3, LOG_ARGS_2)(1, "TEST");
// 2. LOG_ARGS_2(1, "TEST");
// 3. func_args2(1, "TEST");
LOG(ERROR, "TEST", -1);
// 치환 순서
// 1. LOG_SELECTOR(1, "TEST", -1, LOG_ARGS_3, LOG_ARGS_2)(1, "TEST", -1);
// 2. LOG_ARGS_3(1, "TEST", -1);
// 3. func_args3(1, "TEST", -1);
return 0;
}
인자가 1개인 함수에 대해서도 지원을 하려면 마찬가지로 함수와 매크로를 정의해주고 인자 하나만 추가해주면 된다.
결론적으로 가변 인자 매크로 분기를 사용할 때 알아둬야 할 점을 요약해보면 다음과 같다.
1. 인자 FUNC_NAME 앞에 최대 인자 수에 해당하는 더미 인자를 추가해야 한다.
2. LOG_SELECTOR(__VA_ARGS__) 의 뒷부분에 매크로 이름을 인자 수의 내림차순으로 모두 나열해야 한다.
여기까지 기존에 내가 생각했던 구현 요구사항 중 1번과 3번을 충족했다.
✅ 요구사항 1 - 간단히 사용할 수 있다.
✅ 요구사항 3 - 어떤 파일의 몇 번째 줄에서 에러가 발생했는지 출력 할 수 있다.
그렇다면 나머지는 어떻게 충족 했는지에 대한 내용으로 간단히 요약하면 다음과 같다.
✅ 요구사항 2 - hpp에는 함수 선언만 하고, cpp에 함수 정의 및 필요한 다른 함수들을 생성했다.
✅ 요구사항 4, 5
- C++20 에 추가된 라이브러리인 std::format 을 사용하여 조건에 따라 출력 format을 조정했다.
Reference