C++ 정리 - 9 전처리기

전처리기 (Preprocessor)

전처리기(preprocessor)는 프로그램이 컴파일러로 넘어가기 전에 몇 가지 연산을 처리하는 텍스트 처리 장치이다.

  • 전처리기는 텍스트를 구문 분석(parse)하지는 않지만, 매크로 호출을 찾기 위헤 텍스트를 토큰으로 나눈다.
  • 전처리기를 사용하여 프로그램을 조건부로 컴파일하거나, 파일을 끼워넣거나, 컴파일 시간 에러를 특정하거나, 코드에 기계에 따라 다른 규칙을 적용할 수 있다.
  • 일반적으로 컴파일러는 첫 번째 단계에서 전처리기를 호출한다. 하지만 컴파일하지 않고 텍스트를 처리하기 위해 전처리를 별도로 호출할 수도 있다.
  • 마이크로소프트 한정 : /E 또는 /EP 컴파일러 옵션을 사용하여 전처리 후 소스 코드 목록을 얻을 수 있다.
    • /E/EP는 모두 전처리기를 호출하고, 결과 텍스트를 표준 출력 장치(대부분 콘솔)로 보낸다.
    • /E#line 지시문을 포함한다.
    • /EP#line 지시문을 제거한다.

전처리기 유의사항

  • 매크로의 단점들
    • 내가 보는 코드와 컴파일러가 보는 코드가 달라진다.
    • 리팩토링, 분석 도구가 느려지게 한다.
  • 가능한 한 전처리기를 사용하지 말자. constexpr, inline, enum등 좋은 대체제가 많다.
    매크로를 사용하지 말고 대체할 다른 방법을 생각하자. C++은 C와 다르게 매크로가 필수가 아니다.
  • 특히 헤더 파일에서 사용하지 말자. 매크로는 global scope를 가지고 있어 전체 파일들이 더럽혀진다.
  • #define매크로는 사용하기 직전에 만들고, 사용하 후 바로 #undef를 하자.
  • 이미 존재하는 매크로를 #undef하지 말자.
  • 함수/클래스/변수 이름을 만들 때 ##을 피하자.
  • #과 지시문 사이는 붙여쓰자. ex) #include : O, # include : X

전처리기를 사용하면 안 되는 이유

컴파일 에러가 이상하게 뜬다

  • #define으로 상수를 만들어 사용할 때, 해당 상수를 사용한 부분에서 오류가 날 경우, 컴파일 시점을 기준으로 상수는 이미 전처리기에 의해 텍스트로 바뀐 상태이기 때문에 define된 이름으로 에러 메시지가 뜨지 않는다.
  • 반면 constexpr을 사용할 경우 잘 뜬다.

함수의 결과가 예기치 못할 수 있다

  • #define으로 함수를 만들어 사용할 때, 괄호를 잘 사용하지 않으면 우선순위가 뒤바뀐다.
#define mul(x, y) x * y;
int num1 = mul(3 + 4, 2);  // 3 + 4 * 2 로 바뀌어 11이 저장됨

#define add(x, y) x + y;
int num2 = add(2, 3) * 4;  // 2 + 3 * 4 로 바뀌어 14가 저장됨
  • 또한 이 외에도 예상하지 못한 결과를 얻을 수 있다.
#defule square(x) ((x) * (x))
int a = 2;
int num3 = square(a++);  // ((a++) * (a++)) 로 바뀌어 a가 두 번 증가함

그래도 사용해야 하는 전처리기

  • #include는 C++20이 아닌 이상(module이 있음) 어쩔 수 없다.
  • #ifdef로 컴파일 환경에 따라 다르게 컴파일할 때

번역 단계 (Phases of translation)

컴파일러는 파일을 다음 순서로 처리한다 :
(개념 어휘가 많아서 그냥 원문 그대로 넣었다)

  • Character mapping
    Characters in the source file are mapped to the internal source representation. Trigraph sequences are converted to single-character internal representation in this phase.

  • Line splicing
    All lines ending in a backslash (\) immediately followed by a newline character are joined with the next line in the source file, forming logical lines from the physical lines. Unless it's empty, a source file must end in a newline character that's not preceded by a backslash.

  • Tokenization
    The source file is broken into preprocessing tokens and white-space characters. Comments in the source file are replaced with one space character each. Newline characters are retained.

  • Preprocessing
    Preprocessing directives are executed and macros are expanded into the source file. The #include statement invokes translation starting with the preceding three translation steps on any included text.

  • Character-set mapping
    All source character set members and escape sequences are converted to their equivalents in the execution character set. For Microsoft C and C++, both the source and the execution character sets are ASCII.

  • String concatenation
    All adjacent string and wide-string literals are concatenated. For example, "String " "concatenation" becomes "String concatenation".

  • Translation
    All tokens are analyzed syntactically and semantically; these tokens are converted into object code.

  • Linkage
    All external references are resolved to create an executable program or a dynamic-link library.

전처리기 지시문 (Preprocessor directives)

  • 전처리기 지시문은 코드를 서로 다른 환경에서 쉽게 컴파일할 수 있도록 한다.
  • 매크로를 수행(macro expansion)하기 전에 전처리기 라인(Preprocessor line)이 작동하므로 우선순위가 있다.
  • # 전에는 공백(white space)만 와야 한다.
  • 지시문 뒤에 주석(//, /* */)이 올 수 있다.
  • 줄 끝에 지시문에 바로 이어서 백슬래시(\)를 넣으면 전처리기 지시문을 포함하는 줄을 계속 진행시킬 수 있다.
    ex) #include\(한줄뒤)<stdio.h>

#define

#define identifier token-string
#define identifier (identifier, ... , identifier) token-string

  • define은 식별자(identifier)를 토큰 문자열(token-string)로 대체한다.
  • 식별자는 매개변수화(parameterized)될 수 있다.
  • 식별자가 주석이거나, 문자열이거나, 긴 식별자의 일부일 경우 작동하지 않는다.
  • 토큰 문자열의 앞뒤 공백은 토큰 문자열에 포함되지 않는다.
  • 토큰 문자열이 없을 수 있다.
  • 매개변수로 함수처럼 사용할 수 있다.
  • 함수처럼 사용할 때는 괄호를 많이 사용해 버그를 막자.
  • 마이크로소프트 한정 : /D옵션으로도 똑같이 매크로를 defined되게 할 수 있다.
// 잘못된 코드
#define ADD(x, y) x + y

// 버그 : 2 + 3 * 4 가 되어 곱하기가 먼저 계산됨
int res = ADD(2, 3) * 4;

// 이렇게 괄호를 많이 치자.
#define MUL(x, y) ((x) * (y))

#undef

#undef identifier

  • #define을 없앤다.
  • 식별자만 적고 매개변수 리스트는 적지 않는다.
  • 마이크로소프트 한정 : /U옵션으로도 똑같이 매크로를 undefined되게 할 수 있다. 최대 30개.
#define WIDTH 80
#define ADD( X, Y ) ((X) + (Y))

// code

#undef WIDTH
#undef ADD

#error

#error token-string

  • #error지시문은 컴파일 타임에 사용자 정의(user-specified) 에러를 발생시키고 컴파일을 종료한다.
#if !defined(__cplusplus)
#error C++ compiler required.
#endif

#if, #elif, #else, #endif

#if ... #elif ... #else ... #endif

  • 조건부 컴파일(conditional-compilation) 지시문.
  • 뒤의 조건(constant-expression)이 참일 경우(0이 아닐 경우) 아래부터 #endif까지의 코드만 컴파일한다.
  • 각각의 #if지시문(#ifdef포함)은 #endif와 일대일로 대응되어야 한다.
  • #if#endif 사이 #elif는 몇 개를 쓰든 상관없다.
  • #if#endif 사이 #else는 하나만, 그리고 마지막에 있어야 한다.
  • 조건부 컴파일 지시문들은 중첩될 수 있다.
  • 조건부 컴파일 지시문이 include 파일에 포함될 경우 짝이 맞지 않는 조건부 컴파일 지시문이 없도록 하자.
  • 조건은 정수여야 하고, 정수 상수 / 문자 상수 / 정의된 연산자(defined operator) 만 사용할 수 있다.
  • 조건은 sizeof 또는 type-cast 연산자를 사용할 수 없다.
  • 정의된 연산자 (defined operator)

    • defined
      #if defined(CREDIT)  // CREDIT이 #define 되었을 경우 
         credit();
      #elif defined(DEBIT)  // DEBIT #define 되었을 경우 
         debit();
      #else
         printerror();
      #endif
      
      #if !defined(EXAMPLE_H)
      #define EXAMPLE_H
      ...
      #endif
    • __has_include
      아래처럼 쓸 수 있다는데 굳이 알필요없으니 패스
      Visual Studio 2017 version 15.3 and later: Determines whether a library header is available for inclusion:
      #ifdef __has_include
      #  if __has_include(<filesystem>)
      #    include <filesystem>
      #    define have_filesystem 1
      #  elif __has_include(<experimental/filesystem>)
      #    include <experimental/filesystem>
      #    define have_filesystem 1
      #    define experimental_filesystem
      #  else
      #    define have_filesystem 0
      #  endif
      #endif

#ifdef, #ifndef

  • #ifdefine과 같이 쓰는 것과 같다.
  • #ifndef#ifdef의 반대이다. 정의되지 않았을 때만 코드를 컴파일한다.

#import

#include

#include <path-spec>
#include "path-spec"

  • 특정한 파일을 include할 떄 쓰인다.
  • 위의 두 가지 형태를 가지는데, 차이점은 파일의 경로가 불완전하게 특정되었을때 전처리기가 파일을 검색하는 경로의 우선순위이다.
    • <path-spec>의 경우 :
      1. #include가 있는 파일과 같은 디렉터리
      2. #include가 있는 파일의 디렉터리에서 시작해 파일이 include된 역순에 해당하는 파일의 디렉터리들
      3. /I 컴파일러 옵션으로 특정된 경로
      4. INCLUDE 환경 변수로 특정된 경로
    • "path-spec"의 경우 :
      1. /I 컴파일러 옵션으로 특정된 경로
        2, command line에서 컴파일링이 일어날 경우, INCLUDE 환경 변수로 특정된 경로
  • 즉 C, C++ 표준 라이브러리는 <>, 내가 만든 헤더들은 ""로 include 하면 된다.
  • 중첩될 수 있다. file1이 file2를 include하고, file2가 file3를 include할 수 있다.

#line

#line digit-sequence ["filename"]

  • 컴파일을 하다가 오류가 났을 때 오류가 난 부분의 파일 이름과 라인 수를 볼 수 있다.
  • 이 파일 이름과 라인 수를 조작할 수 있다.
  • 라인 수를 바꾸면, 그 다음 줄의 라인 수는, 바뀐 후의 새 라인 수부터 증가하며 이어진다.
// line_directive.cpp
// Compile by using: cl /W4 /EHsc line_directive.cpp
#include <stdio.h>

int main()
{
    printf( "This code is on line %d, in file %s\n", __LINE__, __FILE__ );
#line 10
    printf( "This code is on line %d, in file %s\n", __LINE__, __FILE__ );
#line 20 "hello.cpp"
    printf( "This code is on line %d, in file %s\n", __LINE__, __FILE__ );
    printf( "This code is on line %d, in file %s\n", __LINE__, __FILE__ );
}
// 출력
// This code is on line 7, in file line_directive.cpp
// This code is on line 10, in file line_directive.cpp
// This code is on line 20, in file hello.cpp
// This code is on line 21, in file hello.cpp

널 지시문 (Null diretive)

#

  • # 혼자 있는 지시문이다.
  • 아무 효과가 없다.

#using

#using file [as_friend]

전처리기 연산자 (Preprocessor operators)

  • 전처리기 연산자는 #define문에서 쓰인다.

문자열화 연산자 # (Stringizing operator)

  • #define문에서 매개변수를 토큰 문자열에서 사용할 때 매개변수 이름 바로 앞에 #을 붙여 사용한다.
  • 사용할 경우 매개변수 자리에 넣은 텍스트가 문자열이 되어 들어간다.
  • 매개변수 자리에 넣은 텍스트에 이스케이프 시퀀스가 필요한 경우 이스케이프 백슬래시가 자동으로 삽입된다.
  • 매크로를 실제로 사용할 때 문자열화 연산자가 적용된 인수 앞뒤에 들어간 공백은 무시된다.
  • 매크로를 실제로 사용할 때 문자열화 연산자가 적용된 인수 중간의 연속된 공백은 공백 하나로 처리된다.
  • 마이크로소프트 C++ 문자열화 연산자를 이스케이프 시퀀스가 포함된 문자열과 함께 사용할 때 바르게 작동하지 않는다.
#define stringer( x ) printf_s( #x "\n" )
stringer( In quotes in the printf function call );
stringer( "In quotes when printed to the screen" );
stringer( "This: \"  prints an escaped double quote" );
// 위가 아래로 변환 :
printf_s( "In quotes in the printf function call" "\n" );
printf_s( "\"In quotes when printed to the screen\"" "\n" );
printf_s( "\"This: \\\"  prints an escaped double quote\"" "\n" );

#define AA(x, y) #x    #y
AA(  a    b   , cd)  // "a b""cd"

#define F abc
#define B def
#define FB(arg) #arg
#define FB1(arg) FB(arg)
FB(F B)  // "F B"
FB1(F B)  // "abc def"
// 그냥 매크로를 안 쓰면 고민할 필요 없다..

문자화 연산자 #@ (Charizing operator)

  • 위 문자열화 연산자와 비슷하게 텍스트를 문자로 만든다.
  • 작은따옴표(')는 문자화 연산자에 사용될 수 없다.
#define makechar(x)  #@x
a = makechar(b);  // a = 'b';

토큰 붙여넣기 연산자 ## (Token-pasting operator)

  • 분리된 토큰이 하나의 토큰으로 합쳐지게 한다.
  • merging operator, combining operator 라고도 한다.
  • 매크로는 토큰 붙여넣기 이후에 작동한다.
#define paster( n ) printf_s( "token" #n " = %d", token##n )
int token9 = 9;
paster(9);  // printf_s( "token9 = %d", token9 );

매크로 (Macros)

  • 전처리는 전처리기 지시문을 제외한 모든 행에서 적용된다.
  • 조건부 컴파일로 건너뛰지 않은 부분에서 매크로가 적용된다.
  • 상수를 나타내는 식별자(identifier)를 symbolic constant 혹은 manifest constant라고 부른다.
  • statement나 expression을 나타내는 식별자를 macro라고 부른다.
  • 소스 코드에서 매크로의 이름이 발견되었을 때, 이것을 매크로의 호출(call to macro)로 간주한다.
  • 매크로 호출을 매크로의 몸체를 복사해 대체하는 것을 매크로의 확장(expansion of the macro call)이라고 부른다.

  • 매크로를 한 번 정의하면, 같은 이름으로 다른 값을 정의할 수는 없지만, 같은 이름으로 정확히 같은 내용을 정의할 수 있다.
  • #undef로 정의를 지울 경우 지운 이름으로 다른 값을 정의할 수 있다.

  • C++에선 매크로를 쓰지 말자. 우월한 대체제가 무조건 존재한다. (#include제외)

가변 인자 매크로 (Variadic macros)

  • 인자의 개수가 변하는 function-like 매크로이다.
  • 줄임표(...)로 매크로 정의의 마지막 고정된 인수로 지정할 수 있다.
  • 교체 식별자(replacement identifier) __VA_ARGS__로 추가 인자를 넣을 수 있다.
  • __VA_ARGS__는 줄임표에 들어가는 모든 인자로 대체된다. 이때 인자 사이의 반점들도 포함한다.
  • C 표준은 줄임표에 적어도 한 개의 인자가 들어가도록 한다.
  • 마이크로소프트 C++ 구현에서는 따라오는 반점을 없애 괜찮다.
    /Zc:preprocessor 컴파일러 옵션이 설정되면 반점을 없애지 않는다.
// variadic_macros.cpp
#include <stdio.h>
#define EMPTY

#define CHECK1(x, ...) if (!(x)) { printf(__VA_ARGS__); }
#define CHECK2(x, ...) if ((x)) { printf(__VA_ARGS__); }
#define CHECK3(...) { printf(__VA_ARGS__); }
#define MACRO(s, ...) printf(s, __VA_ARGS__)

int main() {
    CHECK1(0, "here %s %s %s", "are", "some", "varargs1(1)\n");
    CHECK1(1, "here %s %s %s", "are", "some", "varargs1(2)\n");   // won't print

    CHECK2(0, "here %s %s %s", "are", "some", "varargs2(3)\n");   // won't print
    CHECK2(1, "here %s %s %s", "are", "some", "varargs2(4)\n");

    // always invokes printf in the macro
    CHECK3("here %s %s %s", "are", "some", "varargs3(5)\n");

    MACRO("hello, world\n");

    MACRO("error\n", EMPTY); // would cause error C2059, except VC++
                             // suppresses the trailing comma
}
// 출력
// here are some varargs1(1)
// here are some varargs2(4)
// here are some varargs3(5)
// hello, world
// error

미리 정의된 매크로 (Predefined macros)

  • 마이크로소프트 C/C++ 컴파일러 (MSVC)는 특정한 전처리기 매크로를 미리 정의해 높는다.
  • 미리 정의된 매크로들은 언어(C 혹은 C++), 컴파일 대상, 컴파일러 옵션에 따라 달라진다.
  • 미리 정의된 매크로들은 인수를 가지지 않는다.
  • 미리 정의된 매크로들은 재정의될 수 없다.

너무많아서 링크로..
https://docs.microsoft.com/en-us/cpp/preprocessor/predefined-macros?view=msvc-170







출처

1)
2) https://docs.microsoft.com/en-us/cpp/preprocessor/c-cpp-preprocessor-reference?view=msvc-170
3) https://google.github.io/styleguide/cppguide.html#Preprocessor_Macros

내용 수정

좋은 웹페이지 즐겨찾기