번역 단계
번역 단계 (Phases of translation)
C++ 소스 파일은 컴파일러에 의해 처리되어 C++ 프로그램이 돼요. 그런데 이 "처리"는 한 번에 이뤄지는 게 아니라, 정해진 순서의 단계들로 나뉘어 있어요. 이 페이지에서는 소스 코드가 하나의 번역 단위(translation unit)가 되기까지 거치는 과정을 단계별로 살펴봐요.
출처: cppreference
본문
번역 과정 (Translation process)
C++ 프로그램의 텍스트는 **소스 파일(source file)**이라는 단위로 유지돼요.
C++ 소스 파일은 **번역(translation)**을 거쳐 **번역 단위(translation unit)**가 되는데, 다음 단계들로 구성돼요.
- 각 소스 파일을 문자 시퀀스로 매핑.
- 각 문자 시퀀스를 공백으로 구분된 전처리 토큰 시퀀스로 변환.
- 각 전처리 토큰을 토큰으로 변환해 토큰 시퀀스를 형성.
- 각 토큰 시퀀스를 번역 단위로 변환.
C++ 프로그램은 번역된 번역 단위들로 구성될 수 있어요. 번역 단위는 각각 별도로 번역된 뒤 나중에 링크되어 실행 가능한 프로그램을 만들 수 있어요.
이 과정은 **9개의 번역 단계(translation phases)**로 정리될 수 있어요.
전처리 토큰 (Preprocessing tokens)
전처리 토큰은 번역 단계 3부터 6까지에서 언어의 최소 어휘 요소예요.
전처리 토큰의 범주는 다음과 같아요.
- 헤더 이름(header names, 예:
<iostream>이나"myfile.h") - 전처리 import·module 지시문이 만들어내는 플레이스홀더 토큰(즉
import XXX;와module XXX;) (C++20부터) - 식별자(identifiers)
- 전처리 수(preprocessing numbers, 아래 참조)
- 문자 리터럴(user-defined 문자 리터럴 포함, C++11부터)
- 문자열 리터럴(user-defined 문자열 리터럴 포함, C++11부터)
- 연산자와 구분 기호(대체 토큰 포함)
- 다른 어떤 범주에도 맞지 않는 개별 비공백 문자
이 마지막 범주에 해당하는 문자가 다음 중 하나라면 프로그램은 ill-formed예요.
- 아포스트로피(
', U+0027) - 따옴표(
", U+0022) - 기본 문자 집합에 없는 문자
전처리 수 (Preprocessing numbers)
전처리 수의 전처리 토큰 집합은 정수 리터럴과 부동소수점 리터럴 토큰 집합들의 합집합의 상위 집합이에요.
.(선택) digit pp-continue-seq(선택)
- digit — 숫자 0-9 중 하나
- pp-continue-seq — pp-continue들의 시퀀스
각 pp-continue는 다음 중 하나예요.
| 표기 | 의미 |
|---|---|
| identifier-continue | (1) |
| exp-char sign-char | (2) |
. |
(3) |
' digit |
(4) (C++14부터) |
' nondigit |
(5) (C++14부터) |
- identifier-continue — 유효한 식별자의 첫 문자가 아닌 임의의 문자
- exp-char —
P,p(C++11부터),E,e중 하나 - sign-char —
+와-중 하나 - digit — 숫자 0-9 중 하나
- nondigit — 라틴 문자
A/a~`Z/z`와 밑줄 중 하나
전처리 수는 타입이나 값을 갖지 않아요. 성공적으로 정수/부동소수점 리터럴 토큰으로 변환된 뒤에야 둘 다를 얻게 돼요.
공백 (Whitespace)
공백은 주석(comments), 공백 문자, 또는 그 둘로 구성돼요.
다음 문자들이 공백 문자예요.
- 문자 탭(U+0009)
- 줄바꿈(개행 문자, U+000A)
- 수직 탭(U+000B)
- 용지 먹임(U+000C)
- 공백(U+0020)
공백은 보통 전처리 토큰들을 분리하는 데 쓰이는데, 다음 예외가 있어요.
- 헤더 이름, 문자 리터럴, 문자열 리터럴 안에서는 분리자가 아니에요.
- 개행 문자를 포함하는 공백으로 분리된 전처리 토큰들은 전처리 지시문을 형성할 수 없어요.
#include "my header" // OK, 공백을 포함하는 헤더 이름 사용
#include/*hello*/<iostream> // OK, 공백으로 주석 사용
#include
<iostream> // Error: #include는 여러 줄에 걸칠 수 없음
"str ing" // OK, 단일 전처리 토큰(문자열 리터럴)
' ' // OK, 단일 전처리 토큰(문자 리터럴)
최대 먹기 (Maximal munch)
입력이 어느 문자까지 전처리 토큰으로 나뉘었다면, 다음 전처리 토큰은 일반적으로 전처리 토큰을 구성할 수 있는 가장 긴 문자 시퀀스로 취해져요. 설령 그 때문에 이후 분석이 실패할 것처럼 보여도 말이에요. 이것을 흔히 **최대 먹기(maximal munch)**라고 불러요.
int foo = 1;
int bar = 0xE+foo; // Error: 유효하지 않은 전처리 수 0xE+foo
int baz = 0xE + foo; // OK
다시 말해 최대 먹기 규칙은 여러 문자로 된 연산자와 구분 기호 쪽을 지지해요.
int foo = 1;
int bar = 2;
int num1 = foo+++++bar; // Error: "foo++ ++ +baz"로 취급됨, "foo++ + ++baz"가 아님
int num2 = -----foo; // Error: "-- -- -foo"로 취급됨, "- -- --foo"가 아님
최대 먹기 규칙에는 다음 예외들이 있어요.
헤더 이름 전처리 토큰은 다음 경우에만 형성돼요.
#include지시문에서 include 전처리 토큰 뒤__has_include표현식 안에서 (C++17부터)- import 지시문에서 import 전처리 토큰 뒤 (C++20부터)
std::vector<int> x; // OK, "int"는 헤더 이름이 아님
다음 세 문자가 <::이고 그 뒤의 문자가 :도 >도 아니라면, <는 대체 토큰 <:의 첫 문자 대신 그 자체로 하나의 전처리 토큰으로 취급돼요.
struct Foo { static const int v = 1; };
std::vector<::Foo> x; // OK, <:가 [ 의 대체 토큰으로 취급되지 않음
extern int y<::>; // OK, "extern int y[];"와 같음
int z<:::Foo::value:>; // OK, "int z[::Foo::value];"와 같음
(C++11부터) 다음 두 문자가 >>이고 > 문자가 템플릿 식별자를 완성할 수 있다면, 그 문자는 전처리 토큰 >>의 일부 대신 그 자체로 하나의 전처리 토큰으로 취급돼요.
template<int i> class X { /* ... */ };
template<class T> class Y { /* ... */ };
Y<X<1>> x3; // OK, "Y<X<1> >" 타입의 변수 "x3"을 선언
Y<X<6>>1>> x4; // Syntax error
Y<X<(6>>1)>> x5; // OK
(C++11부터) 다음 문자가 원시 문자열 리터럴의 접두사와 초기 큰따옴표가 될 수 있는 문자 시퀀스를 시작한다면, 다음 전처리 토큰은 원시 문자열 리터럴이에요. 그 리터럴은 원시 문자열 패턴과 일치하는 가장 짧은 문자 시퀀스로 구성돼요.
#define R "x"
const char* s = R"y"; // ill-formed 원시 문자열 리터럴, "x" "y"가 아님
const char* s2 = R"(a)" "b)"; // 원시 문자열 리터럴 뒤에 일반 문자열 리터럴
토큰 (Tokens)
토큰은 번역 단계 7에서의 언어의 최소 어휘 요소예요.
토큰의 범주는 다음과 같아요.
- 식별자
- 키워드
- 리터럴
- (전처리 연산자를 제외한) 연산자와 구분 기호
번역 단계 (Translation phases)
번역은 1단계부터 9단계까지의 순서대로 수행되는 것처럼 동작해요. 구현들은 이 별개의 단계들이 일어나는 것처럼 동작하지만, 실제로는 서로 다른 단계가 합쳐질 수도 있어요.
1단계: 소스 문자 매핑 (Mapping source characters)
UTF-8 코드 단위들의 시퀀스인 입력 파일(UTF-8 파일)은 지원이 보장돼요. 그 외에 지원되는 다른 종류의 입력 파일 집합은 구현 정의예요. 그 집합이 비어 있지 않다면, 입력 파일의 종류는 구현 정의 방식으로 결정되는데, 여기에는 내용과 무관하게 입력 파일을 UTF-8 파일로 지정하는 수단(바이트 순서 표시를 인식하는 것은 충분하지 않음)이 포함돼요. (C++23까지는) 삼중자(trigraph) 시퀀스가 대응하는 단일 문자 표현으로 대체됐어요.
(C++23부터) 입력 파일이 UTF-8 파일로 결정되면, 그것은 well-formed UTF-8 코드 단위 시퀀스여야 하고, Unicode 스칼라 값들의 시퀀스로 디코딩돼요. 그런 다음 각 Unicode 스칼라 값을 대응하는 번역 문자 집합 요소로 매핑해 번역 문자 집합 요소들의 시퀀스를 형성해요. 결과 시퀀스에서, 입력 시퀀스의 캐리지 리턴(U+000D) 뒤에 줄바꿈(U+000A)이 이어지는 각 쌍과, 줄바꿈(U+000A)이 바로 뒤따르지 않는 각 캐리지 리턴(U+000D)은 단일 개행 문자로 대체돼요. 구현이 지원하는 다른 종류의 입력 파일에 대해서는, 문자들이 (구현 정의 방식으로) 번역 문자 집합 요소들의 시퀀스로 매핑돼요. 특히 OS 의존적인 줄 끝 표시는 개행 문자로 대체돼요.
2단계: 줄 이어붙이기 (Splicing lines)
- (C++23부터) 첫 번역 문자가 바이트 순서 표시(U+FEFF)이면 그것은 삭제돼요. 백슬래시(
\)가 줄 끝에 나타나면(즉시 뒤에 0개 이상의 공백 문자, 그다음(C++23부터) 줄바꿈 문자가 이어질 때), 이 문자들은 삭제되어 두 물리적 소스 줄을 하나의 논리적 소스 줄로 결합해요. 이것은 단일 패스 연산이에요. 즉 백슬래시 두 개로 끝나고 빈 줄이 뒤따르는 줄은 세 줄을 하나로 결합하지 않아요. - 이 단계 후에 비어 있지 않은 소스 파일이 개행 문자로 끝나지 않는다면(이 시점에는 줄 끝 백슬래시가 더 이상 이어붙이지 않으므로), 종료 개행 문자가 추가돼요.
3단계: 어휘 분석 (Lexing)
- 소스 파일이 전처리 토큰과 공백으로 분해돼요.
// 다음 #include 지시문은 5개의 전처리 토큰으로 분해될 수 있음:
// 구분 기호(#, < 와 >)
// │
// ┌────────┼────────┐
// │ │ │
#include <iostream>
// │ │
// │ └── 헤더 이름 (iostream)
// │
// └─────────── 식별자 (include)
소스 파일이 부분적인 전처리 토큰이나 부분적인 주석으로 끝나면 프로그램은 ill-formed예요.
// Error: 부분적인 문자열 리터럴
"abc
// Error: 부분적인 주석
/* comment
(C++23부터) 소스 파일의 문자들이 다음 전처리 토큰을 형성하기 위해 소비될 때(즉 주석이나 다른 형태의 공백의 일부로 소비되지 않을 때), 유니버설 캐릭터 이름(universal character names)이 인식되어 번역 문자 집합의 지정된 요소로 대체돼요. 다만 다음 전처리 토큰 중 하나에서 문자 시퀀스를 일치시킬 때는 제외돼요.
- 문자 리터럴(c-char-sequence)
- (구분자 d-char-sequence를 제외한) 문자열 리터럴(s-char-sequence와 r-char-sequence)
- 헤더 이름(h-char-sequence와 q-char-sequence)
-
(C++11부터, C++23까지) 원시 문자열 리터럴의 시작과 끝 큰따옴표 사이에서 1단계와 2단계 동안 수행된 변환들은 되돌려져요.
-
공백이 변환돼요.
- 각 주석은 하나의 공백 문자로 대체돼요.
- 개행 문자는 유지돼요.
- 개행 이외의 공백 문자들로 된 각 비어 있지 않은 시퀀스가 유지될지 하나의 공백 문자로 대체될지는 불특정이에요.
4단계: 전처리 (Preprocessing)
- 전처리기가 실행돼요.
#include지시문으로 도입된 각 파일은 재귀적으로 1단계부터 4단계를 거쳐요.- 이 단계 끝에서 모든 전처리 지시문이 소스에서 제거돼요.
5단계: 공통 문자열 리터럴 인코딩 결정 (Determining common string literal encodings)
(C++23까지) 1) 문자 리터럴과 문자열 리터럴의 모든 문자가 소스 문자 집합에서 그 인코딩으로 변환돼요(기본 문자 집합의 96개 문자가 단일 바이트 표현을 갖기만 하면 UTF-8 같은 다중 바이트 문자 인코딩일 수 있어요). 2) 문자 리터럴과 비원시 문자열 리터럴의 이스케이프 시퀀스와 유니버설 캐릭터 이름이 확장되어 리터럴 인코딩으로 변환돼요. (C++23까지) 유니버설 캐릭터 이름이 지정한 문자가 해당 리터럴 인코딩에서 단일 코드 포인트로 인코딩될 수 없다면, 결과는 구현 정의지만 널(와이드) 문자가 아님이 보장돼요.
(C++23부터) 두 개 이상의 인접한 문자열 리터럴 토큰의 시퀀스에 대해, 여기에 설명된 대로 공통 인코딩 접두사가 결정돼요. 그런 다음 각 문자열 리터럴 토큰이 그 공통 인코딩 접두사를 가진 것으로 간주돼요. (문자 변환은 3단계로 이동)
6단계: 문자열 리터럴 연결 (Concatenating string literals)
인접한 문자열 리터럴들이 연결돼요.
7단계: 컴파일 (Compiling)
컴파일이 일어나요. 각 전처리 토큰이 토큰으로 변환돼요. 토큰들은 구문적·의미적으로 분석되고 번역 단위로 번역돼요.
8단계: 템플릿 인스턴스화 (Instantiating templates)
각 번역 단위를 검사해 필요한 템플릿 인스턴스화 목록을 만들고, 여기에는 명시적 인스턴스화가 요청한 것들도 포함돼요. 템플릿의 정의를 찾아 필요한 인스턴스화를 수행해 인스턴스화 단위(instantiation unit)를 만들어요.
9단계: 링크 (Linking)
외부 참조를 충족시키는 데 필요한 번역 단위, 인스턴스화 단위, 라이브러리 구성 요소들이 하나의 프로그램 이미지로 모여요. 이 이미지는 실행 환경에서 실행에 필요한 정보를 담고 있어요.
참고 (Notes)
소스 파일, 번역 단위, 번역된 번역 단위가 반드시 파일로 저장될 필요는 없고, 이 실체들과 어떤 외부 표현 사이에 일대일 대응이 있을 필요도 없어요. 이 설명은 개념적인 것일 뿐 특정 구현을 규정하지 않아요.
(C++23까지) 일부 구현에서는 5단계에서 수행되는 변환을 명령줄 옵션으로 제어할 수 있어요. gcc와 clang은 소스 문자 집합의 인코딩을 지정하는 -finput-charset, 일반/와이드 리터럴 인코딩을 각각 지정하는 -fexec-charset과 -fwide-exec-charset을 쓰고, Visual Studio 2015 Update 2 이상은 소스 문자 집합과 리터럴 인코딩을 각각 지정하는 /source-charset과 /execution-charset을 써요.
일부 컴파일러는 인스턴스화 단위(템플릿 저장소 또는 템플릿 레지스트리라고도 함)를 구현하지 않고, 각 템플릿 인스턴스화를 7단계에서 그냥 컴파일해 암시적이든 명시적이든 요청된 곳에 그 코드를 오브젝트 파일에 저장한 뒤, 링커가 9단계에서 이 컴파일된 인스턴스화들을 하나로 합쳐요.
결함 보고 (Defect reports)
다음의 동작 변경 결함 보고가 이전에 공개된 C++ 표준들에 소급 적용됐어요.
| DR | 적용 | 공개된 동작 | 올바른 동작 |
|---|---|---|---|
| CWG 787 | C++98 | 2단계 끝에서 비어 있지 않은 소스 파일이 개행 문자로 끝나지 않으면 동작이 정의되지 않았음 | 이 경우 종료 개행 문자 추가 |
| CWG 1104 | C++98 | 대체 토큰 <: 때문에 std::vector<::std::string>이 std::vector[:std::string>로 취급됨 |
이 경우를 다루는 추가 어휘 규칙 추가 |
| CWG 1775 | C++11 | 2단계에서 원시 문자열 리터럴 안에 유니버설 캐릭터 이름을 형성하면 정의되지 않은 동작이었음 | well-defined로 만듦 |
| CWG 2747 | C++98 | 2단계가 이어붙이기 후에 파일 끝 이어붙이기를 검사했는데 불필요했음 | 검사 제거 |
| P2621R3 | C++98 | 줄 이어붙이기나 토큰 연결로 유니버설 캐릭터 이름을 형성할 수 없었음 | 허용 |
참조 (References)
- C++23 표준 (ISO/IEC 14882:2024): 5.2 Phases of translation [lex.phases]
- C++20 표준: 5.2 [lex.phases]
- C++17 표준: 5.2 [lex.phases]
- C++14 표준: 2.2 [lex.phases]
- C++11 표준: 2.2 [lex.phases]
- C++03 표준: 2.1 [lex.phases]
- C++98 표준: 2.1 [lex.phases]
같이 보기 (See also)
- C 문서의 Phases of translation
더 알아보기
- 전처리기(Preprocessor): 4단계에서 실행되는 매크로·지시문 처리.
- 주석(Comments): 공백의 일부로 취급되는 영역.
- 문자열 리터럴: 원시 문자열과 이스케이프 시퀀스 처리.