인라인 함수
헤더와 두 C++ 소스의 번역 단위·목적 파일·링크를 먼저 살펴보고 inline의 정의 규칙과 실제 인라인화 판단을 구분합니다.
inline 키워드는 흔히 "함수 호출을 본문으로 바꿔 넣어 빠르게 만드는 키워드"로 소개되지만, 현대 C++에서 inline이 실제로 정하는 것은 함수의 정의를 어디에, 몇 번 둘 수 있는지에 관한 규칙입니다. 호출 자리에 본문을 펼치는 인라인 확장(inline expansion)은 컴파일러의 최적화기가 따로 판단하며, 키워드가 있어도 호출이 남을 수 있고 없어도 펼쳐질 수 있습니다.
이 글은 먼저 함수의 정의와 호출의 선언과 정의를 세 파일로 나누어, 여러 .cpp 파일이 함수를 함께 쓰는 방법을 봅니다. 그다음 정의를 헤더로 옮길 때 inline이 필요한 이유, 실제 인라인 확장을 확인하는 방법, inline이 대신하는 함수형 매크로의 문제를 정리합니다.
inline 함수 선언
함수 앞에 inline 지정자를 붙이면 인라인 함수가 됩니다. 아래 getMax는 반환 타입 앞에 inline이 붙은 것 말고는 보통 함수와 같고, 호출하는 방법도 같습니다. 먼저 파일 하나로 실행해 결과를 확인합니다.
#include <iostream>
inline int getMax(int a, int b) {
return (a > b) ? a : b;
}
int main() {
int x = 10;
int y = 20;
std::cout << getMax(x, y) << '\n';
std::cout << getMax(50, 30) << '\n';
}20
50출력은 함수가 돌려준 값일 뿐, 호출이 본문으로 펼쳐졌는지는 알려 주지 않습니다. 이 프로그램처럼 파일이 하나뿐이면 inline을 빼도 결과와 동작이 같습니다. inline의 의미는 여러 파일이 같은 함수를 쓸 때 드러납니다.
여러 파일에서 일반 함수 쓰기
각 .cpp 파일은 포함한 헤더의 내용과 합쳐져 따로 컴파일되는데, 이 단위를 번역 단위(translation unit)라고 합니다. 아래 예에서는 main.cpp와 my_math.cpp가 각각 번역 단위가 됩니다. 일반 함수는 헤더에 선언하고, 정의는 한 .cpp 파일에만 둡니다.
#pragma once
int add(int a, int b);
int addFromOtherFile();#include "my_math.h"
int add(int a, int b) {
return a + b;
}
int addFromOtherFile() {
return add(30, 40);
}#include <iostream>
#include "my_math.h"
int main() {
std::cout << "sum: " << add(10, 20) << '\n';
std::cout << "from other file: " << addFromOtherFile() << '\n';
}컴파일과 실행 과정에서 본 것처럼, 두 .cpp를 각각 목적 파일로 컴파일한 뒤 함께 링크합니다. 헤더는 각 소스에 포함될 뿐 따로 컴파일하거나 링크하지 않습니다.
g++ -std=c++20 -Wall -Wextra -c main.cpp -o main.o
g++ -std=c++20 -Wall -Wextra -c my_math.cpp -o my_math.o
g++ main.o my_math.o -o inline_demo.exe
.\inline_demo.exesum: 30
from other file: 70main.cpp는 헤더의 선언만 보고 add를 호출하는 코드를 만들고, 링커가 my_math.o에 있는 정의와 연결합니다. 링크 명령에서 my_math.o를 빼면 정의를 찾지 못한 링크 오류가 나므로, 에디터에서 현재 파일만 빌드하는 설정이라면 개발 환경 설정의 안내대로 두 소스를 모두 넘깁니다.
정의를 헤더로 옮기기
이번에는 add의 정의를 헤더에 두어, 두 번역 단위가 같은 정의를 함께 보게 합니다. 이렇게 하려면 정의에 inline을 붙여야 합니다.
#pragma once
inline int add(int a, int b) {
return a + b;
}
int addFromOtherFile();#include "my_math.h"
int addFromOtherFile() {
return add(30, 40);
}main.cpp는 그대로 두고 같은 명령으로 다시 빌드하면 출력은 여전히 sum: 30과 from other file: 70입니다. 헤더로 옮긴 add의 정의는 my_math.cpp에서 지워야 하며, addFromOtherFile은 여전히 선언만 헤더에 있는 일반 함수입니다.
C++ inline 헤더와 두 번역 단위의 링크 흐름
동일 헤더를 두 번역 단위가 포함하고 각각 목적 파일을 만든 뒤 함께 링크한다. 단계마다 main.o/my_math.o 생성 여부가 달라지며 출력 30/70을 인라인 치환 증거로 해석하지 않는다.
HEADER · TRANSLATION UNIT · LINK
두 번역 단위가 같은 inline 정의를 공유한다
헤더에 옮긴 add 정의를 두 .cpp가 함께 사용합니다. 결과가 30과 70이라는 사실만으로 호출 치환이나 성능 이득을 판단할 수는 없습니다.
헤더 포함 → 각 소스 컴파일 → 함께 링크
my_math.h에 정의inline int add(int a, int b)의 본문과 일반 함수addFromOtherFile()의 선언을 둡니다. 옛my_math.cpp의add정의는 제거합니다.헤더를 포함한 번역 단위 두 개
main.cpp는add(10, 20)을 호출하고,my_math.cpp의 래퍼는add(30, 40)을 호출합니다. 두 소스 모두 같은 헤더 정의를 포함합니다.main.o와my_math.oGCC의
-c로 각 소스의 목적 파일을 만듭니다. 헤더는 별도 링크 입력이 아닙니다. 구체적인 함수 코드·심볼 배치는 최적화에 따라 달라질 수 있습니다.하나의
inline_demo.exe로 링크두 목적 파일을 함께 연결합니다. 예상 출력은 합계 30과 다른 파일의 합계 70입니다. 이 일반 헤더 모델에서 inline 정의는 ODR 요건을 만족할 때 하나의 함수로 취급됩니다.
파일 배치가 실제 호출 치환을 강제하지 않는다
분석 가능한 본문 확보
같은 번역 단위에 본문이 있으면 바로 분석할 수 있고, LTO를 사용하면 여러 번역 단위의 정보를 결합할 수도 있습니다.
이익과 비용 비교
본문 크기, 호출 빈도, 상수 인자, 최적화 옵션과 profile 정보, 코드 증가와 instruction-cache 비용을 함께 봅니다.
펼치거나 호출을 유지
add(10, 20)을 펼치거나 상수 계산으로 줄일 수도 있고, 호출을 남길 수도 있습니다. 반환값 30만으로 어느 변환이 있었는지 알 수 없습니다.치환하지 않아도 정의 규칙 유지
inline없는 함수도 펼칠 수 있고 inline 함수의 호출도 남길 수 있습니다. 정의 도달성과 ODR 규칙은 실제 치환 여부와 별개입니다.
범위: 일반 헤더와 두 소스 파일의 예제입니다. #pragma once는 한 번역 단위의 반복 include만 막으며 프로그램 전체의 ODR을 대신하지 않습니다. C++20 named module의 정의 규칙은 본문의 선택 심화에서 구분합니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 6개 노드와 6개 간선이다. 동일 my_math.h가 main.cpp와 my_math.cpp 두 번역 단위에 각각 포함된다. 각 소스가 main.o, my_math.o를 만들고 두 목적 파일 모두 최종 실행 파일로 링크된다. 헤더 자체를 목적 파일이나 별도 링크 입력으로 표시하지 않는다. 예상 출력 30/70은 계산 결과이며 실제 기계 호출의 치환 증거가 아니다. 모바일에서도 동일한 fan-out 두 경로와 fan-in이 유지된다. |
| 노드·연결선 라벨 | 30과 70을 출력 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 공유 선언 두 번역 단위가 같은 헤더를 포함한다. |
|
|
| 별도 컴파일 각 번역 단위가 목적 파일을 만든다. |
|
|
| 링크·실행 목적 파일을 연결한 실행 파일을 실행한다. |
|
|
C++ inline 헤더와 두 번역 단위의 링크 흐름
동일 헤더를 두 번역 단위가 포함하고 각각 목적 파일을 만든 뒤 함께 링크한다. 단계마다 main.o/my_math.o 생성 여부가 달라지며 출력 30/70을 인라인 치환 증거로 해석하지 않는다.
HEADER · TRANSLATION UNIT · LINK
두 번역 단위가 같은 inline 정의를 공유한다
헤더에 옮긴 add 정의를 두 .cpp가 함께 사용합니다. 결과가 30과 70이라는 사실만으로 호출 치환이나 성능 이득을 판단할 수는 없습니다.
헤더 포함 → 각 소스 컴파일 → 함께 링크
my_math.h에 정의inline int add(int a, int b)의 본문과 일반 함수addFromOtherFile()의 선언을 둡니다. 옛my_math.cpp의add정의는 제거합니다.헤더를 포함한 번역 단위 두 개
main.cpp는add(10, 20)을 호출하고,my_math.cpp의 래퍼는add(30, 40)을 호출합니다. 두 소스 모두 같은 헤더 정의를 포함합니다.main.o와my_math.oGCC의
-c로 각 소스의 목적 파일을 만듭니다. 헤더는 별도 링크 입력이 아닙니다. 구체적인 함수 코드·심볼 배치는 최적화에 따라 달라질 수 있습니다.하나의
inline_demo.exe로 링크두 목적 파일을 함께 연결합니다. 예상 출력은 합계 30과 다른 파일의 합계 70입니다. 이 일반 헤더 모델에서 inline 정의는 ODR 요건을 만족할 때 하나의 함수로 취급됩니다.
파일 배치가 실제 호출 치환을 강제하지 않는다
분석 가능한 본문 확보
같은 번역 단위에 본문이 있으면 바로 분석할 수 있고, LTO를 사용하면 여러 번역 단위의 정보를 결합할 수도 있습니다.
이익과 비용 비교
본문 크기, 호출 빈도, 상수 인자, 최적화 옵션과 profile 정보, 코드 증가와 instruction-cache 비용을 함께 봅니다.
펼치거나 호출을 유지
add(10, 20)을 펼치거나 상수 계산으로 줄일 수도 있고, 호출을 남길 수도 있습니다. 반환값 30만으로 어느 변환이 있었는지 알 수 없습니다.치환하지 않아도 정의 규칙 유지
inline없는 함수도 펼칠 수 있고 inline 함수의 호출도 남길 수 있습니다. 정의 도달성과 ODR 규칙은 실제 치환 여부와 별개입니다.
범위: 일반 헤더와 두 소스 파일의 예제입니다. #pragma once는 한 번역 단위의 반복 include만 막으며 프로그램 전체의 ODR을 대신하지 않습니다. C++20 named module의 정의 규칙은 본문의 선택 심화에서 구분합니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 6개 노드와 6개 간선이다. 동일 my_math.h가 main.cpp와 my_math.cpp 두 번역 단위에 각각 포함된다. 각 소스가 main.o, my_math.o를 만들고 두 목적 파일 모두 최종 실행 파일로 링크된다. 헤더 자체를 목적 파일이나 별도 링크 입력으로 표시하지 않는다. 예상 출력 30/70은 계산 결과이며 실제 기계 호출의 치환 증거가 아니다. 모바일에서도 동일한 fan-out 두 경로와 fan-in이 유지된다. |
| 노드·연결선 라벨 | 30과 70을 출력 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 공유 선언 두 번역 단위가 같은 헤더를 포함한다. |
|
|
| 별도 컴파일 각 번역 단위가 목적 파일을 만든다. |
|
|
| 링크·실행 목적 파일을 연결한 실행 파일을 실행한다. |
|
|
그림처럼 같은 헤더를 포함한 두 번역 단위가 각각 목적 파일을 만들고, 두 목적 파일이 하나의 실행 파일로 링크됩니다. 이제 add의 정의가 두 번역 단위에 모두 들어가는데, C++에는 프로그램 전체에서 함수의 정의가 하나여야 한다는 단일 정의 규칙(ODR, One Definition Rule)이 있습니다. inline 함수는 이 규칙의 예외여서, 모든 정의가 공백과 주석을 빼고 같은 코드로 이루어지고 그 안의 이름도 같은 것을 가리키면 여러 번역 단위에 정의를 둘 수 있으며, 프로그램에서는 주소가 하나인 같은 함수로 취급됩니다.
#pragma once는 한 번역 단위 안에서 같은 헤더가 두 번 포함되는 것을 막을 뿐입니다. 서로 다른 .cpp가 같은 헤더를 포함하는 것은 막지 않으므로, 헤더에 본문을 둔 함수의 중복 정의 문제는 #pragma once가 아니라 inline이 해결합니다.
정의가 겹치거나 어긋날 때
헤더로 옮긴 뒤에도 my_math.cpp에 옛 add 정의를 남기면, 그 번역 단위 안에 같은 함수의 정의가 두 개 생겨 컴파일 단계에서 중복 정의 오류가 납니다. 반대로 헤더에 본문을 둔 채 inline만 지우면, 두 번역 단위에 일반 함수 add의 정의가 하나씩 생깁니다. 보통은 링크 단계에서 같은 정의가 여러 개라는 오류가 나지만, 표준은 여러 번역 단위에 걸친 이런 위반을 반드시 진단하라고 요구하지 않습니다.
더 찾기 어려운 경우는 inline 정의의 내용이 번역 단위마다 다를 때입니다. 헤더의 add를 고친 뒤 한쪽 소스만 다시 컴파일하면, 옛 정의로 만든 목적 파일과 새 정의로 만든 목적 파일이 섞입니다. 이것은 단일 정의 규칙을 어긴 것이어서 진단 없이 링크될 수 있고, 어느 정의가 쓰일지 알 수 없습니다. 헤더를 고치면 그 헤더를 포함한 모든 소스를 다시 컴파일해야 하며, 보통은 빌드 도구가 이를 맡습니다.
암시적으로 inline인 것들
inline을 직접 적지 않아도 인라인으로 취급되는 함수가 있습니다. constexpr 함수와, 클래스 정의 안에 본문을 함께 적은 멤버 함수가 그렇습니다. 그래서 이런 함수는 헤더에 정의를 두어 여러 파일에서 써도 중복 정의가 되지 않습니다. C++17부터는 변수에도 inline을 붙일 수 있어, 헤더에 정의한 전역 변수를 여러 파일이 하나의 변수로 함께 쓸 수 있습니다.
함수 템플릿도 보통 헤더에 정의를 두지만, 이것은 템플릿 자체의 규칙으로 허용되므로 inline을 붙일 필요는 없습니다. 또 C++20의 모듈(module) 안에 정의한 함수는 클래스 안에 정의해도 암시적으로 inline이 되지 않는 등 규칙이 다르므로, 이 글의 설명은 헤더와 소스 파일로 나누는 일반적인 구성에 한합니다.
실제 인라인 확장은 컴파일러가 정합니다
호출 자리에 함수 본문을 펼치는 인라인 확장은 최적화기가 호출 지점마다 판단합니다. 본문의 크기, 그 호출이 얼마나 자주 실행되는지, 펼친 뒤 상수 계산이나 분기 제거로 더 줄어드는지, 최적화 수준과 대상 CPU, 그리고 본문을 복사해 늘어나는 코드 크기를 함께 봅니다.
그래서 inline을 붙여도 펼쳐지지 않을 수 있고, inline이 없는 일반 함수도 같은 번역 단위에서 호출되면 펼쳐질 수 있습니다. 링크 시간 최적화(LTO, -flto)를 켜면 다른 .cpp에 정의한 일반 함수도 펼쳐질 수 있습니다. 재귀 함수나 함수 포인터를 통한 호출도 경우에 따라 일부가 펼쳐지므로, 함수의 모양 하나만으로 결과를 단정하지 않습니다.
최적화를 끈 -O0 빌드는 보통 호출을 그대로 두고, -O2 이상에서 인라인 확장이 활발해집니다. GCC는 -fopt-info-inline-optimized로 펼친 호출을, -fopt-info-inline-missed로 펼치지 않은 호출과 그 이유를 보고합니다. 실제로 펼쳐졌는지 알려면 이런 최적화 보고나 생성된 어셈블리를 확인해야 합니다.
더 깊이: 표준의 힌트 문구와 컴파일러별 강제 지정
표준 문서에는 inline의 원래 뜻도 남아 있습니다. 표준 초안의 inline 절은 inline 지정자가 호출 지점에서 본문을 대신 넣는 편이 보통의 호출보다 낫다는 것을 구현에 알린다고 하면서, 구현이 그렇게 할 의무는 없다고 함께 적습니다. 그래서 inline은 지금도 힌트이기는 하지만 따를지는 컴파일러가 정하고, 반드시 지켜지는 것은 이 글에서 본 정의의 규칙입니다. GCC는 -O2부터 inline이 없는 함수도 인라인 후보로 보는 -finline-functions를 켜므로, 힌트의 영향은 더 작아집니다.
힌트보다 강하게 정하고 싶을 때는 컴파일러마다 다른 확장을 씁니다. GCC의 __attribute__((always_inline))은 inline으로 선언한 함수를 다른 인라인 제한과 상관없이 펼치게 하고, 펼치지 못하면 오류로 알립니다. 반대로 __attribute__((noinline))은 그 함수를 인라인 후보에서 빼며, 함수 사이의 다른 최적화도 일부 꺼집니다. Clang도 두 속성을 지원합니다.
MSVC에는 __forceinline과 [[msvc::forceinline]]이 있지만, Microsoft 문서는 이것으로도 인라인 확장을 강제할 수는 없다고 밝힙니다. 디버그 빌드의 기본값인 /Ob0로 컴파일했거나, 함수가 가변 인자를 받거나, 함수 포인터를 통해 부르는 경우에는 펼쳐지지 않습니다. 펼치지 못하면 수준 1 경고를 내지만, /Od나 /Ob0로 컴파일했거나 함수가 다른 번역 단위에 정의되었거나 간접 호출의 대상인 경우에는 경고하지 않습니다. 어느 컴파일러든 강제 지정은 측정으로 효과를 확인한 좁은 곳에만 씁니다.
함수형 매크로와 비교
C와 오래된 C++ 코드에서는 호출 비용을 피하려고 함수 모양의 매크로를 쓰기도 했습니다. 아래 두 매크로는 모두 인자를 제곱하려는 것인데, SQUARE는 인자와 전체 식을 괄호로 감쌌고 BAD_SQUARE는 괄호가 없다는 점만 다릅니다.
#define SQUARE(x) ((x) * (x))
#define BAD_SQUARE(x) x * x매크로는 함수가 아니라, 컴파일 전의 전처리 단계에서 글자를 바꿔 넣는 것입니다. SQUARE(1 + 2)는 ((1 + 2) * (1 + 2))가 되어 9이지만, 괄호를 빠뜨린 BAD_SQUARE(1 + 2)는 1 + 2 * 1 + 2가 되어 연산자 우선순위에 따라 5가 됩니다. 매크로에는 타입 검사가 없고, 이름 공간도 무시하며, 오류가 펼쳐진 코드에서 보고되어 원인을 찾기도 어렵습니다.
인자가 여러 번 계산되는 것도 문제입니다. SQUARE(i++)는 ((i++) * (i++))가 되어 한 식에서 i를 두 번 바꾸므로 정의되지 않은 동작입니다. 함수는 인자를 한 번만 계산하므로 이런 문제가 없습니다. 조건부 컴파일처럼 전처리기가 꼭 필요한 일이 아니라면, 매크로 대신 일반 함수나 constexpr 함수, 함수 템플릿을 씁니다.
성능을 확인하는 순서
성능 때문에 인라인 확장을 검토한다면 inline을 늘리는 것부터 시작하지 않습니다. 아래 그림처럼 작고 자주 실행되는 함수는 좋은 후보일 수 있지만, 큰 함수를 여러 곳에 펼치면 실행 파일이 커지고 명령어 캐시 부담이 늘어 오히려 느려질 수 있습니다.
CANDIDATE · COST · EVIDENCE
인라인화 후보의 실제 이득은 속도와 코드 크기로 확인한다
함수 크기나 호출 형태 하나만으로 실제 인라인화 여부를 단정할 수 없습니다. optimizer가 보는 문맥과 release 설정을 맞춘 뒤 runtime, code size, cache 영향을 함께 확인합니다.
작고 뜨거우며 caller 문맥이 이익을 만든다
return a + b; 같은 짧은 본문이 hot path에서 반복되고, 상수 전파나 분기 제거처럼 치환 뒤의 추가 최적화가 기대되면 좋은 후보입니다. 본문이 보이거나 LTO가 정보를 가져올 수 있어야 합니다.
크고 차가운 함수는 코드 증가가 이익을 덮을 수 있다
큰 본문을 여러 호출 지점에 복제하면 binary와 instruction-cache 부담, 컴파일 시간, 디버깅 난도가 커질 수 있습니다. “루프 안 호출”이나 inline 표기만으로 hot하다고 가정하지 않습니다.
재귀·간접 호출·복잡한 제어 흐름도 문맥을 본다
- 재귀 호출을 무한히 펼칠 수는 없지만 비재귀 call site, 제한된 깊이 또는 일부 경로는 인라인화될 수 있습니다.
- 함수 포인터나 virtual 호출도 target이 밝혀지거나 devirtualize되면 직접 호출로 바뀌어 후보가 될 수 있습니다.
- 큰 루프나
switch는 비용을 높일 뿐 절대 금지 규칙은 아니며 partial inlining도 가능합니다. - 정상적으로 한
.cpp에 정의한 일반 함수도 같은 번역 단위의 호출이나 LTO 분석에서는 인라인화될 수 있습니다. 이는 inline 정의의 reachability 규칙을 대신하지 않습니다.
함수·template을 텍스트 치환과 혼동하지 않는다
함수는 scope와 type checking을 가지며 인자를 한 번씩 평가합니다. 반면 SQUARE(i++) 같은 매크로는 인자를 두 번 치환해 unsequenced modification 같은 undefined behavior를 만들 수 있습니다.
조건부 컴파일, 문자열화, token 결합처럼 preprocessing이 필요한 경우가 아니라면 일반 함수, constexpr 함수나 template을 먼저 선택합니다.
키워드가 아니라 증거로 유지 여부를 결정한다
비교 조건 고정
실제 target, release 최적화 옵션, LTO·PGO 설정과 대표 입력을 맞춘 기준 binary를 준비합니다.
hot call site 확인
profiler로 시간이 쓰이는 호출을 찾고 compiler optimization remark나 생성 assembly에서 치환·미치환 이유를 확인합니다.
속도와 크기를 함께 비교
runtime뿐 아니라 binary 크기와 instruction-cache 지표도 비교합니다. 개선이 재현되지 않으면 표기나 강제 attribute를 늘리지 않습니다.
판정 순서: 언어 계약을 먼저 맞춘 뒤 optimizer에 후보를 맡기고, profile과 최적화 기록으로 실제 병목에서 이득이 있었는지 확인합니다.
먼저 실제로 배포할 최적화 옵션과 대표적인 입력으로 빌드를 고정하고, 프로파일러로 시간이 많이 드는 호출 지점을 찾습니다. 그다음 최적화 보고나 어셈블리로 그 호출이 펼쳐졌는지와 펼쳐지지 않은 이유를 확인하고, 실행 시간과 함께 실행 파일의 크기도 비교합니다. 측정 없이 붙인 inline은 성능에 아무 영향이 없거나, 헤더 의존성만 늘릴 수 있습니다.
정리
inline이 정하는 것은 정의의 배치 규칙이며, 같은inline정의를 여러 번역 단위에 둘 수 있게 합니다.- 일반 함수는 헤더에 선언하고 정의를 한
.cpp에 두며, 헤더에 본문을 두려면inline이 필요합니다. #pragma once는 한 번역 단위 안의 중복 포함만 막고, 여러 번역 단위의 중복 정의는 막지 않습니다.- 번역 단위마다 다른
inline정의는 단일 정의 규칙 위반이며 진단되지 않을 수 있으므로, 헤더를 고치면 포함한 모든 소스를 다시 컴파일합니다. - 호출을 본문으로 펼치는 인라인 확장은 최적화기가 정하며, GCC의
-fopt-info-inline보고나 어셈블리로 확인합니다. - 함수형 매크로는 타입 검사가 없고 인자를 여러 번 계산할 수 있으므로, 일반 함수나
constexpr함수로 대신합니다.
연습 문제
1. 헤더를 고친 뒤 다시 빌드하기
inline 버전 헤더의 add 본문을 return a + b + 1;로 바꾸고 두 소스를 모두 다시 컴파일해 링크하면 출력은 무엇인가요? 한쪽 소스만 다시 컴파일하면 왜 문제가 되나요?
해설 보기
add(10, 20)은 31, add(30, 40)은 71이므로 sum: 31과 from other file: 71이 출력됩니다. 한쪽만 다시 컴파일하면 옛 정의로 만든 목적 파일과 새 정의로 만든 목적 파일이 섞여 단일 정의 규칙을 어기게 되고, 링크가 성공해도 어느 정의가 쓰일지 알 수 없습니다. 출력 31과 71은 바뀐 계산을 확인할 뿐, 호출이 펼쳐졌는지는 알려 주지 않습니다.
2. 괄호 없는 매크로
BAD_SQUARE(1 + 2), SQUARE(1 + 2), 10 / SQUARE(2)의 값은 각각 무엇인가요?
해설 보기
BAD_SQUARE(1 + 2)는 1 + 2 * 1 + 2로 펼쳐져 5, SQUARE(1 + 2)는 ((1 + 2) * (1 + 2))여서 9입니다. 10 / SQUARE(2)는 10 / ((2) * (2))로 펼쳐져 2이며, 바깥 괄호가 없었다면 10 / (2) * (2)가 되어 10이 됩니다. 매크로는 이처럼 괄호를 하나라도 빠뜨리면 결과가 바뀌므로, 함수로 쓰는 편이 안전합니다.
3. inline을 지운 헤더
inline 버전 헤더에서 inline만 지우면 빌드는 어떻게 되나요? main.cpp 하나만 이 헤더를 포함하는 프로그램이었다면 어떤가요?
해설 보기
main.cpp와 my_math.cpp가 모두 헤더를 포함하므로 일반 함수 add의 정의가 두 번역 단위에 하나씩 생기고, 보통 링크 단계에서 같은 정의가 여러 개라는 오류가 납니다. 이 헤더를 포함하는 번역 단위가 하나뿐이라면 정의도 하나여서 문제없이 빌드됩니다. 하지만 나중에 다른 파일이 헤더를 포함하는 순간 오류가 나므로, 헤더에 본문을 두는 함수에는 inline을 붙입니다.
다음 글 기본 매개변수에서는 인자를 생략하면 미리 정한 값이 들어가는 기본 매개변수와, 그것을 선언할 위치를 다룹니다.