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
- defined
#ifdef, #ifndef
#if
를define
과 같이 쓰는 것과 같다.#ifndef
는#ifdef
의 반대이다. 정의되지 않았을 때만 코드를 컴파일한다.
#import
- type library에서 정보를 가져올 때 쓴다
- 아래를 참조하자
https://docs.microsoft.com/en-us/cpp/preprocessor/hash-import-directive-cpp?view=msvc-170
#include
#include <path-spec>
#include "path-spec"
- 특정한 파일을 include할 떄 쓰인다.
- 위의 두 가지 형태를 가지는데, 차이점은 파일의 경로가 불완전하게 특정되었을때 전처리기가 파일을 검색하는 경로의 우선순위이다.
<path-spec>
의 경우 :#include
가 있는 파일과 같은 디렉터리#include
가 있는 파일의 디렉터리에서 시작해 파일이 include된 역순에 해당하는 파일의 디렉터리들/I
컴파일러 옵션으로 특정된 경로INCLUDE
환경 변수로 특정된 경로
"path-spec"
의 경우 :/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]
/clr
로 컴파일 된 프로그램에 metadata를 import한다.- 뭔 말인지 모르겠어서 링크
https://docs.microsoft.com/en-us/cpp/preprocessor/hash-using-directive-cpp?view=msvc-170
전처리기 연산자 (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
내용 수정
Author And Source
이 문제에 관하여(C++ 정리 - 9 전처리기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@chorogchip/Cpp-정리-9-전처리기저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)