Conditionals

전처리기 조건 지시자는 특정 코드나 지시자를 선택적으로 제외하거나 포함할 때 사용합니다.

  • machine 또는 operating system 에 따라 설정을 달리해 컴파일해야 될 때

  • 같은 소스코드로 다른 프로그램을 만들고자 할 때 ( 테스트 코드나, 디버깅 정보 출력을 포함하거나 제외 )

  • 특정 코드 영역을 삭제하지 않고 나중에 참조하기 위해 제외하려고 할 때 등

#ifdef, #ifndef

#ifdef, #ifndef 지시자는 특정 매크로가 현재 정의되어 있는지 테스트할 때 사용합니다. 다음과 같이 object-like 매크로, function-like 매크로 모두 #ifdef 에서 참이 됩니다.

$ gcpp                      $ gcpp                         $ gcpp
#define foo                 #define foo 100                #define foo()

#ifdef foo                  #ifdef foo                     #ifdef foo
111                         111                            111
#endif                      #endif                         #endif
@                           @                              @

111                         111                            111

같은 소스코드를 컴파일 하더라도 machine 또는 operating system 에 따라 predefined macros 가 다를 수 있습니다. 또한 명령행 상에서 -D 옵션을 이용해 매크로를 define 하고 -U 옵션을 이용해 undefine 할 수도 있습니다. 다음은 #ifndef 지시자를 이용해서 Once-Only Headers 를 구현하는 예입니다.

// foo.h 파일 내용
// 처음 foo.h 파일이 include 될때 FILE_FOO_H 매크로가 #define 되므로
// 두 번째부터는 #ifndef 지시자에 의해 다시 include 되지 않게 됩니다.

#ifndef FILE_FOO_H
#define FILE_FOO_H

the entire file

#endif

#if

# if Expression                   // C expression of integer type

    controlled text . . .

# endif

#ifdef 지시자는 단순히 특정 매크로가 정의되어 있는지 테스트 하지만 #if 지시자는 expression 을 다룹니다. expression 에서는 C 언어에서 사용되는 대부분의 연산자를 사용할 수 있습니다. ( ++, --, 포인터 관련, 대입연산자 들은 제외 ). 다음은 #if,#else 조건 지시자를 이용해 선택적으로 매크로를 정의하는 예인데 &&, ||, ! 논리연산자, <, > 비교연산자, a ? b : c 삼항연산자 들이 사용되고 있는 것을 볼 수 있습니다.

// __USE_GNU 매크로가 정의되어 있고 && (defined __cplusplus ? (...) : (...)) 가 참이면
// __GLIBC_USE_DEPRECATED_SCANF 매크로가 1 로 정의되고 그렇지 않으면 0 으로 정의됩니다.

#if (defined __USE_GNU              \
     && (defined __cplusplus        \
     ? (__cplusplus < 201103L && !defined __GXX_EXPERIMENTAL_CXX0X__)   \
     : (!defined __STDC_VERSION__ || __STDC_VERSION__ < 199901L)))
# define __GLIBC_USE_DEPRECATED_SCANF 1
#else
# define __GLIBC_USE_DEPRECATED_SCANF 0
#endif

expression 에 매크로가 사용되면 먼저 확장이 완료된 후 연산이 됩니다. 정의되지 않은 매크로 사용, 0 은 거짓과 같고 그 외 값은 참이 됩니다. body 가 empty 인 매크로를 사용하면 expression 이 empty 가 되므로 오류가 됩니다. 기본적으로 전처리 단계에서는 C/C++ 언어의 타입 정보는 알 수 없으므로 sizeof 연산자나 enum 값은 사용할 수 없습니다. expression 에서 사용할 수 있는 값들은 다음과 같습니다.

  • integer constants
  • character constants ( 'A' 는 65 와 같다 )
  • defined 전처리기 연산자의 사용
  • 정의되지 않은 매크로는 0 과 같습니다.
  • double quotes 은 오류에 해당합니다.
$ gcpp               $ gcpp                    $ gcpp                 $ gcpp -Dfoo
#if foo              #define foo               #define foo bar        #if foo
#define NUM 100      #if foo                   #if foo                #define NUM 200
#endif               #define NUM 200           #define NUM 200        #endif
                     #endif                    #endif
value: NUM                                                            value: NUM
@                    value: NUM                value: NUM             @
                     @                         @
value: NUM           <stdin>:2:8: error:                              value: 200
                     #if with no expression    value: NUM
# 정의되지 않은                                   # foo 가 bar 로 확장되고
# 매크로는             value: NUM                # bar 는 정의되지 않은
# 0 과 같다.                                     # 매크로가 된다.
----------------------------------------------------------------------------------

$ gcpp                            $ gcpp                         $ gcpp
#define foo                       #define foo (1 - 1)            #define foo (1 + 1)
#if defined(foo)                  #if foo                        #if foo
#define NUM 200                   #define NUM 200                #define NUM 100
#endif                            #endif                         #endif

value: NUM                        value: NUM                     value: NUM
@                                 @                              @

value: 200                        value: NUM                     value: 100
-------------------------------------------------------------------------------------

$ gcpp                            $ gcpp                          $ gcpp
#define foo "bar"                 #define foo 'bar'               #define foo 'b'
#if foo                           #if foo                         #if foo
#define NUM 200                   #define NUM 200                 #define NUM 200
#endif                            #endif                          #endif

value: NUM                        value: NUM                      value: NUM
@                                 @                               @
<stdin>:1:13: error:              <stdin>:2:5: warning:
token ""bar"" is not valid        multi-character character       value: 200
in preprocessor expressions       constant [-Wmultichar]

value: NUM                        value: 200

#if 0 ( always-false )

소스코드의 특정 영역을 삭제하지 않고 나중에 참조하기 위해 남겨놓으려 할 경우, /*...*/ ( block comment ) 는 nesting 해서 사용할 수가 없기 때문에 오류가 발생할 수 있는데 이때 #if 0 을 활용할 수 있습니다. #if 0 에 의해 코드가 제외된다고 하더라도 quotes 이나 /*...*/ 는 balance 가 맞아야 합니다.

#if 0

"controlled"
'text'                     // ' or " 가 하나만 있으면 warning 이 되고
/* controlled text */      // /* 하나만 있으면 error 가 된다.

#endif

defined

defined 는 전처리기 연산자로 #if, #elif 지시자를 사용할 때 특정 매크로가 현재 정의되어 있는지 테스트할 때 사용합니다. #ifdef MACRO 는 사실상 #if defined MACRO 와 같은 것입니다. logical not 은 앞에 ! 를 붙이면 됩니다.

#if defined FOO || defined (BAR)      // 괄호는 제외하거나 포함할 수 있다.

정의되지 않은 매크로는 0 과 같습니다. 따라서 다음 첫 번째 식은 두 번째와 같이 줄일 수 있습니다.

#if defined BUFSIZE && BUFSIZE >= 1024

#if BUFSIZE >= 1024

#elif

#elif 지시자 뒤에는 #else 가 와도 되고 안 와도 됩니다.

#else

#else 지시자는 #if, #elif 뿐만 아니라 #ifdef, #ifndef 지시자 와도 함께 사용할 수 있습니다.

#if , #else 가 중첩될 경우 적용되는 순서는 다음과 같습니다.

$ gcpp
#define FEATURE_A

#if defined (FEATURE_A)
AAA
#if defined (FEATURE_B)
BBB
#if defined (FEATURE_C)
CCC
#else
ELSE_CCC
#endif
#else
ELSE_BBB
#endif
#else
ELSE_AAA
#endif
@

AAA
ELSE_BBB

#endif

#if, #ifdef, #ifndef 지시자를 사용할 때 종료를 나타냅니다.

#if __linux__                            // #if
#define OFFSET    9
#define OFFSIZ    8
#define MAP    "maps"
#define PTX    long
#define DIFF_T    void *

#elif __FreeBSD__                        // #elif
#define OFFSET    10
#define OFFSIZ    9
#define MAP    "map"
#define PTX    int
#define DIFF_T    caddr_t
#define PTRACE_TRACEME     PT_TRACE_ME
#define PTRACE_PEEKTEXT    PT_READ_I
#define PTRACE_PEEKDATA    PT_READ_D
#define PTRACE_KILL     PT_KILL

#else                                    // #else
#error / This system do not support.
#endif                                   // #endif

Quiz

assert 매크로를 이용하면 인수로 주어진 표현식이 거짓일 경우 프로그램 실행을 중단시킬 수 있습니다. 하지만 이것은 실행 중에나 가능한 runtime 테스트인데요. 매크로를 활용해서 compile-time 에 테스트를 해서 빌드를 중단시키려면 어떻게 할까요?

$ gcc -xc - <<\@ && ./a.out
#define assert(expr) ((void) sizeof ((expr) ? 1 : 0), __extension__ \
   ({ if (expr) ; else __assert_fail (#expr, __FILE__, __LINE__, __ASSERT_FUNCTION); }))
#define __ASSERT_FUNCTION __extension__ __PRETTY_FUNCTION__
extern void __assert_fail (const char *__assertion, const char *__file,
      unsigned int __line, const char *__function)
     __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__noreturn__));

int main() { assert(1 != 1); }        // assert 매크로는 runtime 테스트 이다.
@

a.out: <stdin>:7: main: Assertion `1 != 1' failed.
-----------------------------------------------------------------------------

(void) sizeof ((expr) ? 1 : 0) 의 결과값은 ',' 연산자 왼쪽에 있기 때문에
사용되지 않는 것입니다 ( 명시적으로 (void) 로 캐스팅 했죠 ).
expr 에서 발생하는 warning 메시지 출력에 사용되고 ',' 연산자 다음식이 이어서 실행됩니다. 
좀더 자세한 설명은 /usr/include/assert.h 파일을 참고하세요.

__PRETTY_FUNCTION__ 은 __func__ , __FUNCTION__ 와 같은 것인데 C++ 에서는 차이가 있습니다.

리눅스 커널 소스에 보면 BUILD_BUG_ON_ZERO(e) 매크로가 있는데요. 이 매크로를 사용하면 표현식 e 의 결과가 0 이 아닐 경우 컴파일 오류가 발생하여 빌드가 중단됩니다.

linux/include/linux/build_bug.h 참조

$ gcc -xc -fsyntax-only - <<\@
#define BUILD_BUG_ON_ZERO(e) ((int)(sizeof(struct { int:(-!!(e)); })))
int X = BUILD_BUG_ON_ZERO(0);      // 0 일 경우는 정상적으로 빌드가 진행되지만 
@
OK

$ gcc -xc -fsyntax-only - <<\@
#define BUILD_BUG_ON_ZERO(e) ((int)(sizeof(struct { int:(-!!(e)); })))
int X = BUILD_BUG_ON_ZERO(3);      // 0 이 아닐 경우는 컴파일 오류가 발생하여 빌드가 중단된다.
@
<stdin>:1:51: error: negative width in bit-field ‘<anonymous>’
<stdin>:2:9: note: in expansion of macro ‘BUILD_BUG_ON_ZERO’

---------------------------------------------------------------

sizeof(struct { int: -!!(e); }))

표현식 (e) 앞에 ! (not) 이 두 번 붙으면 결과값이 무엇이든 0 아니면 1 로 되게 됩니다.
그 앞에 다시 - (minus) 가 붙었으니 최종 결과는 다음 두 가지가 될 수 있습니다.

sizeof(struct { int: 0; })       // -0 은 0 과 같으므로 
sizeof(struct { int: -1; })

bit-field width 값으로 0 은 오류가 되지 않으므로 정상적으로 컴파일이 진행되지만
-1 의 경우는 컴파일 오류가 발생하여 빌드가 중단되게 됩니다.

VERIFY_OCTAL_PERMISSIONS(perms) 매크로는 BUILD_BUG_ON_ZERO(e) 매크로를 사용하여 인수로 주어진 permission 값이 특정 조건을 만족하는지 체크합니다. BUILD_BUG_ON_ZERO(e) 매크로에서 오류가 발생하지 않을 경우 확장 값은 0 과 같게되므로 모든 테스트가 통과하면 VERIFY_OCTAL_PERMISSIONS(perms) 매크로의 최종 확장 값은 전달된 인수 그대로 (perms) 이 되게 됩니다.

linux/include/linux/kernel.h 참조

$ gcc -xc -fsyntax-only - <<\@
#define BUILD_BUG_ON_ZERO(e) ((int)(sizeof(struct { int:(-!!(e)); })))

#define VERIFY_OCTAL_PERMISSIONS(perms)                                         \
        (BUILD_BUG_ON_ZERO((perms) < 0) +                                       \
         BUILD_BUG_ON_ZERO((perms) > 0777) +                                    \
         /* USER_READABLE >= GROUP_READABLE >= OTHER_READABLE */                \
         BUILD_BUG_ON_ZERO((((perms) >> 6) & 4) < (((perms) >> 3) & 4)) +       \
         BUILD_BUG_ON_ZERO((((perms) >> 3) & 4) < ((perms) & 4)) +              \
         /* USER_WRITABLE >= GROUP_WRITABLE */                                  \
         BUILD_BUG_ON_ZERO((((perms) >> 6) & 2) < (((perms) >> 3) & 2)) +       \
         /* OTHER_WRITABLE?  Generally considered a bad idea. */                \
         BUILD_BUG_ON_ZERO((perms) & 2) +                                       \
         (perms))

int X = VERIFY_OCTAL_PERMISSIONS(0666);      // 0666 permission 은 안된다.
@
<stdin>:1:51: error: negative width in bit-field ‘<anonymous>’
<stdin>:12:3: note: in expansion of macro ‘BUILD_BUG_ON_ZERO’
<stdin>:15:9: note: in expansion of macro ‘VERIFY_OCTAL_PERMISSIONS’

----------------------------------------------------------------------

$ gcc -xc -fsyntax-only - <<\@
#define BUILD_BUG_ON_ZERO(e) ((int)(sizeof(struct { int:(-!!(e)); })))

#define VERIFY_OCTAL_PERMISSIONS(perms)                                         \
        (BUILD_BUG_ON_ZERO((perms) < 0) +                                       \
         BUILD_BUG_ON_ZERO((perms) > 0777) +                                    \
         /* USER_READABLE >= GROUP_READABLE >= OTHER_READABLE */                \
         BUILD_BUG_ON_ZERO((((perms) >> 6) & 4) < (((perms) >> 3) & 4)) +       \
         BUILD_BUG_ON_ZERO((((perms) >> 3) & 4) < ((perms) & 4)) +              \
         /* USER_WRITABLE >= GROUP_WRITABLE */                                  \
         BUILD_BUG_ON_ZERO((((perms) >> 6) & 2) < (((perms) >> 3) & 2)) +       \
         /* OTHER_WRITABLE?  Generally considered a bad idea. */                \
         BUILD_BUG_ON_ZERO((perms) & 2) +                                       \
         (perms))

int X = VERIFY_OCTAL_PERMISSIONS(0644);       // 0644 는 OK
@

OK

문c 블로그 참조 http://jake.dothome.co.kr/device-driver-1/

linux/include/linux/sysfs.h  참조

#define __ATTR(_name, _mode, _show, _store) {               \
    .attr = {.name = __stringify(_name),                    \
    .mode = VERIFY_OCTAL_PERMISSIONS(_mode) },              \
    .show   = _show,                                        \
    .store  = _store,                                       \
}

__ATTR() 매크로에서 사용하는 VERIFY_OCTAL_PERMISSIONS() 매크로는 
컴파일 타임에 파일 권한 값이 다음의 조건을 만족하는지 체크한다.

   - 0000 ~ 0777 범위 이내
   - USER_READABLE >= GROUP_READABLE >= OTHER_READABLE
        * 0444 (ok)
        * 0440 (ok)
        * 0400 (ok)
        * 0000 (ok)
        * 0404 (X)
        * 0004 (X)
        * 0040 (X)
        * 가장 많이 실수하는 값은 예전에는 허용했던 0666 값이 최근에는 
          OTHER_WRITABLE 때문에 문제가 발생한다. 이를 0644 등으로 수정해야 한다.
   - USER_WRITABLE >= GROUP_WRITABLE
        * 0220 (ok)
        * 0200 (ok)
        * 0000 (ok)
        * 0020 (X)
        * OTHER_WRITABLE 안됨!
            * 0XX0 (ok)
            * 0XX2 (X)

compiletime assert 방법에는 gcc 의 error function attribute 를 사용하는 방법도 있습니다. 이것도 리눅스 커널에서 사용되고 있는 방법인데, 오류와 관련해서 메시지를 설정할 수 있습니다. 이 방법은 확장 결과가 statement 가 되므로 대입 연산에서는 사용할 수 없습니다.

$ gcc -c -xc - <<\@
#define compiletime_assert(condition, msg)                                      \
    do {                                                                        \
        extern void _compiletime_assert(void) __attribute__(( error( #msg )));  \
        if (!(condition))                                                       \
            _compiletime_assert();                                              \
    } while (0)

int main() { compiletime_assert(2 < 1, "2 is definitely bigger than 1"); }
@

<stdin>: In function ‘main’:
<stdin>:5:13: error: call to ‘_compiletime_assert’ declared with attribute error: 
"2 is definitely bigger than 1"
<stdin>:8:14: note: in expansion of macro ‘compiletime_assert’

대입 연산에서 사용하려면 다음과 같이 statement expressions 을 이용하면 되는데 이 방법은 enum 값이나 bit-field width 같은 상수가 사용되는 곳에서는 사용할 수 없고 함수 밖에서도 사용할 수 없습니다.

$ gcc -c -xc - <<\@
#define compiletime_assert(condition, msg)                                      \
    ({                                                                          \
        extern void _compiletime_assert(void) __attribute__(( error( #msg )));  \
        if (!(condition))                                                       \
            _compiletime_assert();                                              \
        (condition);                                                            \
    })
int main() { int X = compiletime_assert(2 < 1, "2 is definitely bigger than 1"); }
@

<stdin>: In function ‘main’:
<stdin>:5:13: error: call to ‘_compiletime_assert’ declared with attribute error: 
"2 is definitely bigger than 1"
<stdin>:8:22: note: in expansion of macro ‘compiletime_assert’