본문으로 이동

C의 프리프로세서

위키책, 위키책

프리프로세서(이하 전처리기)란, C 프로그램이 컴파일되기 이전에 텍스트 프로세싱을 하는 방법을 말합니다.
여기서 텍스트 프로세싱이란 헤더 파일을 포함하거나 매크로 상수 및 함수를 만드는 등의 행위를 뜻합니다.
모든 C 프로그램은 실제로 컴파일되기 이전 전처리 과정을 거칩니다.
전처리 과정은 프로그램을 훑어보고 '프로세서 지시문'이라고 하는 컴파일러가 이해 가능한 명령어를 찾아냅니다.
모든 프로세서 지시문은 #(해시) 기호로 시작합니다. 또한, C++ 컴파일러도 C와 같은 전처리기를 사용합니다.

전처리는 컴파일러가 보기 전에 코드에 예비 연산(조건부 컴파일 코드, 파일 등)을 수행하는 컴파일 과정의 일부입니다.
이러한 과정은 전처리기의 출력이 여전히 텍스트라는 것을 의미합니다.

참고사항: 기술적으로 C의 전처리 단계 출력은 소스 텍스트가 아닌 토큰 시퀀스 구조로 구성됩니다.
그러나 소스 텍스트는 토큰 시퀀스에 모두 대응되므로, 컴파일러 또한 보통 -E/E 옵션을 통해 원하는 방식으로 바꿀 수 있도록 지원합니다.
비록 이러한 C 컴파일러에 대한 명령줄 옵션이 완전히 표준화된 것은 아니지만, 많은 컴파일러들이 이와 비슷한 규칙을 따르고 있습니다.

지시문

[+/-]

지시문은 소스 코드의 일부 또는 전부를 처리하거나 최종 개체에 일부 플래그를 설정하는 방법에 대한 전처리기 또는 컴파일러에 대한 특별 지시사항입니다.
이러한 지시문은 코드 작성을 더 쉽고 보기 좋게 만들 수 있습니다.
이해할 수 있는 명령어는 전처리기에 의해 처리되는데, 여기서 전처리기는 컴파일러에 의해 호출되거나 컴파일러에 의해 호출되는 별도의 프로그램입니다.

#include

[+/-]

C는 모든 표준 호환 C 컴파일러와 함께 사용할 수 있는 코드의 저장소인 표준 라이브러리의 일부로서 여러 기능들을 가지고 있습니다.
그리고 C 컴파일러는 당신의 프로그램을 컴파일할 때, 일반적으로 표준 C 라이브러리와 연결시킵니다.
예를 들어, #include <stdio.h>지시문을 실제 <stdio.h> 헤더 파일의 내용으로 바꿔줍니다.

라이브러리에서 기능을 사용할 때, C는 당신이 그 기능을 사용하겠다고 '선언(Declaration)'하는 것을 권장합니다.
따라서 프로그램의 첫 번째 줄은 보통 전처리 지시문으로, 다음과 같이 표시되어야 합니다.

 #include <stdio.h>

위 코드는 stdio.h에 있는 C 선언을 포함합니다.
다시 말해 stdio.h라고 불리는 헤더 파일의 내용을 당신의 프로그램에 삽입함으로써 구현된다고 할 수 있으며, 이 경우 시스템의 종속 위치에 있다고 합니다.
이러한 헤더 파일의 위치는 컴파일러의 설명서에 기재되어 있고 표준 C 헤더 파일 목록은 헤더 표에 나와 있습니다.

stdio.h 헤더는 스트림(Stream)이라고 불리는 입출력 메커니즘의 추상화를 사용하여 입출력(I/O)을 위한 다양한 선언을 포함합니다.
예를 들어 텍스트를 표준 출력으로 출력하는 데 사용되는 stdout이라는 출력 스트림 개체가 있는데, 이는 일반적으로 텍스트를 컴퓨터 화면에 표시합니다.

위의 예와 같은 화살 괄호(< >)를 사용하는 경우, 전처리기는 표준 라이브러리 포함 설정에 따른 환경 경로를 자동으로 찾아가 포함할 파일을 검색합니다.

 #include "other.h"

위 코드와 같이 따옴표(" ")를 사용하는 경우, 전처리기는 사용자 정의 위치에서 검색하며 해당 위치에 없는 경우에만 표준 라이브러리 포함 경로를 찾아봅니다.
일반적으로 이러한 따옴표 기재 방식은 #include 지시문을 사용하는 파일과 동일한 디렉터리에서의 검색을 뜻합니다.

참고사항: #include 지시문의 구현은 컴파일러마다 다르므로 사용 중인 개발 환경의 설명서를 확인해야 합니다.

Headers

[+/-]

C90의 표준 헤더 목록

C90 이후 추가된 헤더 목록

#pragma

[+/-]

pragma(실용적인 정보) 지시문은 표준의 일부이지만, 사용되는 표준의 소프트웨어 구현에 따라 의미가 달라집니다.
#pragma 지시문은 컴파일러로부터 특별한 동작을 요청하는 방법을 제공합니다.
또한 이 명령은 비정상적으로 코드가 크거나 특정 컴파일러의 기능을 활용해야 하는 프로그램에 가장 유용합니다.

pragma는 소스 프로그램 내에서 사용됩니다.

 #pragma (토큰)

#pragma는 일반적으로 컴파일러가 따라야 할 명령을 나타내는 단일 토큰이 뒤따르며, 지원되는 토큰 목록은 사용할 C 표준의 소프트웨어 구현을 확인해야 합니다.
참고로 #pragma 지시문에 나타나는 명령어 집합은 컴파일러마다 다릅니다.
따라서 컴파일러가 허용하는 명령어와 기능을 확인하려면 해당 컴파일러의 설명서를 참조해야 합니다.

예를 들어, 가장 많이 사용되는 전처리 지시문 중 하나인 #pragma once는 헤더 파일의 시작 부분에 위치해야 하며 전처리기 과정에서 여러 번 포함될 경우 해당 파일이 건너뛰어진다는 것을 나타냅니다.

참고사항: 일반적으로 include guards기능을 사용하여 #pragma once를 수행하는 방법도 있습니다.

#define

[+/-]

경고: 전처리기 매크로는 매력적인 기능이지만 제대로 처리되지 않을 경우 예상치 못한 결과를 초래할 수 있습니다.
매크로는 컴파일되기 전에 소스 코드를 미리 정의해 둔 텍스트로 대체하는 기능일 뿐이라는 점을 항상 염두에 두십시오.
컴파일러는 매크로에 대해 아무것도 모르기 때문에 매크로를 볼 수 없습니다.
이로 인해 다른 부정적인 결과 중에서도 상당히 불명확한 오류가 발생할 수 있습니다.
이와 비슷한 기능을 언어 차원의 기능으로서 제공한다면, 그것을 사용하는 것을 권장합니다(예: #define (상수) 대신 const int (상수) 또는 enum을 사용하십시오).

물론 매크로가 매우 유용한 경우도 있습니다(예: 아래 debug 매크로 참조).

#define 지시문은 프로그램 소스 코드를 컴파일하기 전에 전처리기에 의해 상수나 매크로 함수를 정의하기 위해 사용됩니다.
전처리기의 정의들은 컴파일러가 소스 코드를 보기 전에 대체되기 때문에, #define에 의해 생겨난 오류들은 추적하기 어렵습니다.

관례상 #define를 사용하여 정의된 값은 모두 대문자로 씁니다.
그렇게 하는 것이 필수는 아니지만, 그렇게 하지 않는 것은 매우 나쁜 관행으로 여겨집니다.
모두 대문자여야 소스 코드를 읽을 때 매크로임을 쉽게 식별할 수 있기 때문입니다.

요즘엔 #define이 주로 컴파일러와 플랫폼 차이를 다루는 데 사용됩니다.
예를 들어 시스템 호출에 적합한 (상수로 된) 오류 값의 용도로 사용할 수 있습니다.
따라서 #define의 사용은 정말로 필요하지 않는 한 사용하지 말아야 합니다.
typedef 문과 상수 선언문은 보통 동일한 기능을 더 안전하게 수행할 수 있습니다.

#define 명령어의 또 다른 특징은 매개변수를 받을 수 있다는 것입니다.
다음의 코드를 참고하십시오.

 #define ABSOLUTE_VALUE(x) ( ((x) < 0) ? -(x) : (x) )
 
 ...
 int x = -1;
 while(ABSOLUTE_VALUE(x)) {
     ...
 }

복잡한 매크로를 사용할 때는 보통 추가적인 괄호를 사용하는 것이 좋습니다.
위의 예제에서 변수 x는 항상 괄호 안에 있습니다.
이 방법을 사용하면 0과 비교하거나 -1을 곱하기 전에 x의 값이 평가됩니다.
또한 전체 매크로가 다른 코드에 의해 오염되는 것을 방지하기 위해 괄호로 둘러싸여 있습니다.
이는 괄호를 사용하지 않는 경우 컴파일러가 코드를 잘못 해석할 위험이 있기 때문입니다.

이러한 부작용 때문에 위에서 설명한 것처럼, 매크로 기능을 사용하는 것은 매우 나쁜 생각으로 여겨집니다.

 int x = -10;
 int y = ABSOLUTE_VALUE(x++);

만약 ABLUTLE_VALUE()가 (매크로가 아닌)진짜 함수였다면, x의 값은 -9가 되겠지만, 매크로의 매개변수로 들어갔기 때문에 두 번 평가되어(x++(x)로 들어가기 때문에) 값은 -8이 됩니다.

예제:
매크로의 위험성을 이해하기 위해, 이 간단한 매크로를 보십시오.

 #define MAX(a, b) a > b ? a : b

그리고 그 매크로를 이렇게 사용했다고 합시다.

 i = MAX(2, 3) + 5;
 j = MAX(3, 2) + 5;

이 경우 컴파일러가 보게 될 코드는 이렇게 대체될 것입니다.

 int i = 2 > 3 ? 2 : 3 + 5;
 int j = 3 > 2 ? 3 : 2 + 5;

따라서 실행 후 예상 결과는 ij 모두 8이 되는 것이었으나, i는 8, j는 3이 되어버리고 맙니다!
이러한 이유로 위에서 괄호를 추가로 사용하도록 주의를 했던 것입니다.
하지만 이렇게 괄호를 사용했더라도 이 방식은 온갖 위험으로 가득 차 있습니다.

당신은 이제 a 또는 b가 식을 포함한다면, 매크로 정의에서 a와 ,b를 사용할 시 괄호로 묶어야 한다는 것을 금방 알아차릴 수 있을 것입니다.
그러나 다음의 예제를 보십시오.

 #define MAX(a, b) ((a) > (b) ? (a) : (b))

이렇게만 사용한다면 확실히 부작용은 없어 보일 수 있습니다.

 i = 2;
 j = 3;
 k = MAX(i++, j++);

하지만 이렇게 사용한다면, 결과는 k는 4가 되고, i는 3이 되며 심지어 j는 5가 됩니다.
MAX()가 진짜 함수처럼 작동할 줄 알았던 사람이 이 결과를 본다면 굉장히 어이없을 것입니다.

그렇다면 정말로 정확한 방법은 무엇일까요?
그냥 매크로를 사용하지 않는 것입니다.
이렇게 전역 인라인 함수를 정의하면 됩니다.

 inline int max(int a, int b) {
   return a > b ? a : b;
 }

이 경우 위의 매크로처럼 이상하게 작동하지는 않지만, 모든 타입에서 작동하지 않는다는 단점이 있기는 합니다.

참고사항: 정의가 헤더 파일에 없지 않은 한 명시적으로 inline 키워드를 쓸 필요는 없습니다.
왜냐하면 컴파일러는 당신을 위해 함수를 자동으로 인라인해줄 수 있기 때문입니다(gcc는 -finline-functions 또는 -O3으로 할 수 있습니다).
컴파일러는 종종 프로그래머보다 어떤 함수가 인라인할 가치가 있는지를 더 잘 예측합니다.
또한 함수 호출은 실제로 그리 비싸지도 않습니다.

컴파일러는 실제로 inline 키워드를 무시할 수 있습니다.
이는 단지 컴파일러에게 주는 힌트(또는 건의 사항)일 뿐입니다.

(#, ##)

### 연산자는 #define 매크로와 함께 사용됩니다.
#을 사용하면 # 뒤에 있는 첫 번째 매개변수가 큰따옴표로 감싸진 것처럼 문자열로 반환됩니다.
예제 코드를 보자면

 #define as_string(s) # s

이 매크로의 경우 매크로 프로세서는

 puts(as_string(Hello World!));

이 코드를

 puts("Hello World!");

이렇게 변환시킬 것입니다.

##을 사용하면 ## 앞에 있는 매개변수와 뒤에 있는 매개변수를 연결할 수 있습니다.
예제 코드를 보자면

 #define concatenate(x, y) x ## y
 ...
 int xy = 10;
 ...

이 매크로의 경우 매크로 프로세서는

 printf("%d", concatenate(x, y));

이 코드를

 printf("%d", xy);

이렇게 변환시킬 것입니다.
결과적으로 10을 출력합니다.

또한 매크로 인수를 일정한 접두사 또는 접미사처럼 사용하여 다음과 같이 유용하게 사용할 수 있습니다.

 #define make_function(name) int my_ ## name (int foo) {}
 make_function(bar)

결과는 my_bar() 함수가 될 것입니다.
하지만 연결 연산자를 사용해서 매크로 매개변수를 하나의 상수 문자열로 통합하는 것은 불가능합니다.
만약 그렇게 하고 싶다면 ANSIC 속성을 사용해 두 개 이상의 연속 문자열을 단일 문자열과 동일한 것으로 간주할 수 있습니다.

상수 문자열로 통합할 수 없는 것의 예제는 이렇습니다.

 #define eat(what) puts("I'm eating " #what " today.")
 eat( fruit );

이러한 코드를 매크로 프로세서는

 puts( "I'm eating " "fruit" " today." );

이렇게 C 파서에 의해 단일 문자열 여러 개로 변환시킬 것입니다.

다음 방법은 숫자 상수를 문자열 리터럴로 변환하는 데 사용할 수 있습니다.

 #define num2str(x) str(x)
 #define str(x) #x
 #define CONST 23
 
 puts(num2str(CONST));

이 경우 2단계로 확장되기 때문에 조금 까다롭습니다.
첫 번째 num2str(CONST)str(23)로 대체되며, 다시 23으로 대체됩니다.
이러한 기능은 다음 예제에 유용하게 사용될 수 있습니다.

 #ifdef DEBUG
 #define debug(msg) fputs(__FILE__ ":" num2str(__LINE__) " - " msg, stderr)
 #else
 #define debug(msg)
 #endif

이렇게 하면 파일 이름과 에러 줄 위치가 포함된 유용한 디버그 메시지를 표시할 수 있습니다.
DEBUG 컴파일 매개변수가 정의되지 않으면 디버깅 메시지가 코드에서 완전히 사라집니다.
이는 유용하지만 컴파일 매개변수 설정에 따라 나타났다가 사라지는 버그가 발생할 수 있으므로, 부작용이 우려되면 사용하지 않도록 주의하십시오.

매크로

[+/-]

매크로는 컴파일러에 의해 검사되지 않으므로 매개변수를 평가하지 않습니다.
또한 코드의 구역을 제대로 따르지 않고 단순히 전달된 문자열을 가져와서 각 매크로 매개변수에 해당하는 실제 문자열로 대체할 뿐입니다.(코드는 문자 그대로 호출된 위치에 복사 & 붙여넣기 됩니다).

매크로의 작동 방식 예제를 봅시다.

 #include <stdio.h>

 #define SLICES 8
 #define ADD(x) ((x) / SLICES)

 int main(void) 
 {
   int a = 0, b = 10, c = 6;

   a = ADD(b + c);
   printf("%d\n", a);
   return 0;
 }

이 경우 결과적으로 x의 값으로 b + c가 들어갔으나 (x)처럼 괄호를 사용했으므로 정상적으로 a는 2가 됩니다.

참고사항: 일반적으로 머리글에 매크로를 정의하는 것은 좋지 않습니다.
따라서 기능 또는 다른 메커니즘으로 대체할 수 없는 경우에만 사용해야 합니다.
일부 컴파일러는 작은 함수에 대한 호출을 인라인 코드로 대체하는 등 코드를 최적화할 수 있습니다(다만 이로 인해 나오는 속도 이점은 없습니다).
따라서 typedef, enum 및 인라인(C99) 함수를 사용하는 것이 종종 더 나은 옵션이 됩니다.

인라인 함수가 작동하지 않는 몇 안 되는 상황 중 하나는 컴파일 과정에서 사용해야 할 상수(정적 구조체 등)를 초기화해야 하는 경우입니다.
이것은 매크로에 대한 매개변수가 컴파일러의 다른 값에 의해 최적화될 수 있는 값일 경우 발생합니다. [1]

#error

[+/-]

#error 명령은 컴파일을 중지합니다.
표준에 명시된 에러가 났음을 나타내는 어떠한 토큰이 발견될 경우, 이는 컴파일러가 명령어의 남은 토큰을 포함하는 메시지를 출력해야 한다고 알려줍니다.
따라서 #error는 대부분 디버깅 목적으로 사용됩니다.

프로그래머는 조건부 블록 안에서 #error를 사용하여 블록의 시작 부분에서 #if 또는 #ifdef가 컴파일 과정에서 문제를 감지하면 컴파일러를 즉시 중지하도록 만듭니다.
일반적으로 컴파일러는 블록(및 블록 내부의 #error 지시)을 건너뛰고 컴파일을 진행합니다.

  #error message

#warning

[+/-]

많은 컴파일러가 #warning 명령을 지원합니다.
경고를 일으키는 어떠한 토큰이 발견되면, 컴파일러는 지시문의 나머지 토큰을 포함하는 메시지를 출력합니다.

  #warning message

#undef

[+/-]

#undef 지시문은 매크로를 따로 정의하지 않습니다.
따라서 식별자를 미리 정의할 필요는 없습니다.

#if,#else,#elif,#endif (조건부 지시문)

[+/-]

#if 지시문은 해당 조건식이 0으로 평가되는지 또는 0이 아닌지를 확인하고 각각 코드 블록을 제외하거나 포함합니다.
예제 코드를 봅시다.

 #if 1
    /* 이 구역은 포함됩니다. */
 #endif
 #if 0
    /* 이 구역은 포함되지 않습니다. */
 #endif

여기서 조건식에는 할당 연산자와 증감 연산자, 주소 연산자, sizeof() 연산자를 제외한 모든 C 연산자를 사용할 수 있습니다.

전처리에 사용되는 유일한 연산자는 defined 연산자입니다.
즉, defined(macro)로 묶인 매크로가 현재 정의되어 있으면 1을 반환하고, 정의되어 있지 않으면 0을 반환합니다.

#endif 지시문은 #if, #ifdef 또는 #ifndef로 시작하는 블록을 종료하는 데 쓰입니다.
또한 #elif 지시문은 코드 블록을 한 번 더 나눌 수 있다는 점에서 #if와 유사합니다.
예제 코드를 봅시다.

 #if /* 조건문 */
   :
   :
   :
 #elif /* 또 다른 조건문 */
   :
 /* 추가로 #elif를 얼마든지 넣을 수 있습니다. */
   :
 #else
 /* #else는 #if을 만족하지 않는 경우에 관한 처리를 할 때 또는
    #elif 지시문을 사용했을 때 사용합니다. */
   :
   :
 #endif /* 여기가 #if의 끝입니다. */

#ifdef,#ifndef

[+/-]

The #ifdef command is similar to #if, except that the code block following it is selected if a macro name is defined. In this respect,

#ifdef NAME

is equivalent to

#if defined NAME


The #ifndef command is similar to #ifdef, except that the test is reversed:

#ifndef NAME

is equivalent to

#if !defined NAME

#line

[+/-]

This preprocessor directive is used to set the file name and the line number of the line following the directive to new values. This is used to set the __FILE__ and __LINE__ macros.

Useful Preprocessor Macros for Debugging

[+/-]

ANSI C defines some useful preprocessor macros and variables,[2][3] also called "magic constants", include:

__FILE__ => The name of the current file, as a string literal
__LINE__ => Current line of the source file, as a numeric literal
__DATE__ => Current system date, as a string
__TIME__ => Current system time, as a string
__TIMESTAMP__ => Date and time (non-standard)
__cplusplus => undefined when your C code is being compiled by a C compiler; 199711L when your C code is being compiled by a C++ compiler compliant with 1998 C++ standard.
__func__ => Current function name of the source file, as a string (part of C99)
__PRETTY_FUNCTION__ => "decorated" Current function name of the source file, as a string (in GCC; non-standard)

Compile-time assertions

[+/-]

Compile-time assertions can help you debug faster than using only run-time assert() statements, because the compile-time assertions are all tested at compile time, while it is possible that a test run of a program may fail to exercise some run-time assert() statements.

Prior to the C11 standard, some people[4][5][6] defined a preprocessor macro to allow compile-time assertions, something like:

#define COMPILE_TIME_ASSERT(pred) switch(0){case 0:case pred:;}

COMPILE_TIME_ASSERT( BOOLEAN CONDITION );

The static_assert.hpp Boost library defines a similar macro.[7]

Since C11, such macros are obsolete, as _Static_assert and its macro equivalent static_assert are standardized and built-in to the language.

X-Macros

[+/-]

틀:Merge to

One little-known usage pattern of the C preprocessor is known as "X-Macros".[8][9][10][11] An X-Macro is a header file or macro. Commonly these use the extension ".def" instead of the traditional ".h". This file contains a list of similar macro calls, which can be referred to as "component macros". The include file is then referenced repeatedly in the following pattern. Here, the include file is "xmacro.def" and it contains a list of component macros of the style "foo(x, y, z)".

#define foo(x, y, z) doSomethingWith(x, y, z);
#include "xmacro.def"
#undef foo

#define foo(x, y, z) doSomethingElseWith(x, y, z);
#include "xmacro.def"
#undef foo

(etc...)

The most common usage of X-Macros is to establish a list of C objects and then automatically generate code for each of them. Some implementations also perform any #undefs they need inside the X-Macro, as opposed to expecting the caller to undefine them.

Common sets of objects are a set of global configuration settings, a set of members of a struct, a list of possible XML tags for converting an XML file to a quickly-traversable tree, or the body of an enum declaration; other lists are possible.

Once the X-Macro has been processed to create the list of objects, the component macros can be redefined to generate, for instance, accessor and/or mutator functions. Structure serializing and deserializing are also commonly done.

Here is an example of an X-Macro that establishes a struct and automatically creates serialize/deserialize functions. For simplicity, this example doesn't account for endianness or buffer overflows.

File star.def:

EXPAND_EXPAND_STAR_MEMBER(x, int)
EXPAND_EXPAND_STAR_MEMBER(y, int)
EXPAND_EXPAND_STAR_MEMBER(z, int)
EXPAND_EXPAND_STAR_MEMBER(radius, double)
#undef EXPAND_EXPAND_STAR_MEMBER

File star_table.c:

typedef struct {
  #define EXPAND_EXPAND_STAR_MEMBER(member, type) type member;
  #include "star.def"
  } starStruct;

void serialize_star(const starStruct *const star, unsigned char *buffer) {
  #define EXPAND_EXPAND_STAR_MEMBER(member, type) \
    memcpy(buffer, &(star->member), sizeof(star->member)); \
    buffer += sizeof(star->member);
  #include "star.def"
  }

void deserialize_star(starStruct *const star, const unsigned char *buffer) {
  #define EXPAND_EXPAND_STAR_MEMBER(member, type) \
    memcpy(&(star->member), buffer, sizeof(star->member)); \
    buffer += sizeof(star->member);
  #include "star.def"
  }

Handlers for individual data types may be created and accessed using token concatenation ("##") and quoting ("#") operators. For example, the following might be added to the above code:

#define print_int(val)    printf("%d", val)
#define print_double(val) printf("%g", val)

void print_star(const starStruct *const star) {
  /* print_##type will be replaced with print_int or print_double */
  #define EXPAND_EXPAND_STAR_MEMBER(member, type) \
    printf("%s: ", #member); \
    print_##type(star->member); \
    printf("\n");
  #include "star.def"
  }

Note that in this example you can also avoid the creation of separate handler functions for each datatype in this example by defining the print format for each supported type, with the additional benefit of reducing the expansion code produced by this header file:

#define FORMAT_(type) FORMAT_##type
#define FORMAT_int    "%d"
#define FORMAT_double "%g"

void print_star(const starStruct *const star) {
  /* FORMAT_(type) will be replaced with FORMAT_int or FORMAT_double */
  #define EXPAND_EXPAND_STAR_MEMBER(member, type) \
    printf("%s: " FORMAT_(type) "\n", #member, star->member);
  #include "star.def"
  }

The creation of a separate header file can be avoided by creating a single macro containing what would be the contents of the file. For instance, the above file "star.def" could be replaced with this macro at the beginning of:

File star_table.c:

#define EXPAND_STAR \
  EXPAND_STAR_MEMBER(x, int) \
  EXPAND_STAR_MEMBER(y, int) \
  EXPAND_STAR_MEMBER(z, int) \
  EXPAND_STAR_MEMBER(radius, double)

and then all calls to #include "star.def" could be replaced with a simple EXPAND_STAR statement. The rest of the above file would become:

typedef struct {
  #define EXPAND_STAR_MEMBER(member, type) type member;
  EXPAND_STAR
  #undef  EXPAND_STAR_MEMBER
  } starStruct;

void serialize_star(const starStruct *const star, unsigned char *buffer) {
  #define EXPAND_STAR_MEMBER(member, type) \
    memcpy(buffer, &(star->member), sizeof(star->member)); \
    buffer += sizeof(star->member);
  EXPAND_STAR
  #undef  EXPAND_STAR_MEMBER
  }

void deserialize_star(starStruct *const star, const unsigned char *buffer) {
  #define EXPAND_STAR_MEMBER(member, type) \
    memcpy(&(star->member), buffer, sizeof(star->member)); \
    buffer += sizeof(star->member);
  EXPAND_STAR
  #undef  EXPAND_STAR_MEMBER
  }

and the print handler could be added as well as:

#define print_int(val)    printf("%d", val)
#define print_double(val) printf("%g", val)

void print_star(const starStruct *const star) {
  /* print_##type will be replaced with print_int or print_double */
  #define EXPAND_STAR_MEMBER(member, type) \
    printf("%s: ", #member); \
    print_##type(star->member); \
    printf("\n");
  EXPAND_STAR
  #undef EXPAND_STAR_MEMBER
}

or as:

#define FORMAT_(type) FORMAT_##type
#define FORMAT_int    "%d"
#define FORMAT_double "%g"

void print_star(const starStruct *const star) {
  /* FORMAT_(type) will be replaced with FORMAT_int or FORMAT_double */
  #define EXPAND_STAR_MEMBER(member, type) \
    printf("%s: " FORMAT_(type) "\n", #member, star->member);
  EXPAND_STAR
  #undef EXPAND_STAR_MEMBER
  }

A variant which avoids needing to know the members of any expanded sub-macros is to accept the operators as an argument to the list macro:

File star_table.c:

/*
 Generic
 */
#define STRUCT_MEMBER(member, type, dummy) type member;

#define SERIALIZE_MEMBER(member, type, obj, buffer) \
  memcpy(buffer, &(obj->member), sizeof(obj->member)); \
  buffer += sizeof(obj->member);

#define DESERIALIZE_MEMBER(member, type, obj, buffer) \
  memcpy(&(obj->member), buffer, sizeof(obj->member)); \
  buffer += sizeof(obj->member);

#define FORMAT_(type) FORMAT_##type
#define FORMAT_int    "%d"
#define FORMAT_double "%g"

/* FORMAT_(type) will be replaced with FORMAT_int or FORMAT_double */
#define PRINT_MEMBER(member, type, obj) \
  printf("%s: " FORMAT_(type) "\n", #member, obj->member);

/*
 starStruct
 */

#define EXPAND_STAR(_, ...) \
  _(x, int, __VA_ARGS__) \
  _(y, int, __VA_ARGS__) \
  _(z, int, __VA_ARGS__) \
  _(radius, double, __VA_ARGS__)

typedef struct {
  EXPAND_STAR(STRUCT_MEMBER, )
  } starStruct;

void serialize_star(const starStruct *const star, unsigned char *buffer) {
  EXPAND_STAR(SERIALIZE_MEMBER, star, buffer)
  }

void deserialize_star(starStruct *const star, const unsigned char *buffer) {
  EXPAND_STAR(DESERIALIZE_MEMBER, star, buffer)
  }

void print_star(const starStruct *const star) {
  EXPAND_STAR(PRINT_MEMBER, star)
  }

This approach can be dangerous in that the entire macro set is always interpreted as if it was on a single source line, which could encounter compiler limits with complex component macros and/or long member lists.

This technique was reported by Lars Wirzenius[12] in a web page dated January 17, 2000, in which he gives credit to Kenneth Oksanen for "refining and developing" the technique prior to 1997. The other references describe it as a method from at least a decade before the turn of the century.


We discuss X-Macros more in a later section, Serialization and X-Macros.

틀:Wikipediapar 틀:Reflist

틀:C Programming/Navigation

  1. David Hart, Jon Reid. "9 Code Smells of Preprocessor Use". 2012.
  2. HP C Compiler Reference Manual
  3. C++ reference: Predefined preprocessor variables
  4. "Compile Time Assertions in C" by Jon Jagger 1999
  5. Pádraig Brady. "static assertion".
  6. "ternary operator with a constant (true) value?".
  7. Wikipedia: C++0x#Static assertions
  8. Wirzenius, Lars. C Preprocessor Trick For Implementing Similar Data Types Retrieved January 9, 2011.
  9. 틀:Cite journal
  10. 틀:Cite journal
  11. Keith Schwarz. "Advanced Preprocessor Techniques". 2009. Includes "Practical Applications of the Preprocessor II: The X Macro Trick".
  12. Wirzenius, Lars. C Preprocessor Trick For Implementing Similar Data Types Retrieved January 9, 2011.