← 모든 글

C++ 빌드 과정으로 이해하는 inline과 Chromium의 작성 지침

헤더에 한 번 정의한 함수가 중복되는 이유를 빌드 과정과 inline의 규칙으로 살펴봅니다.

C++Chromium

『열혈 C++ 프로그래밍』에서 인라인 함수를 공부하면서, 함수 호출에 드는 비용을 줄이기 위해 inline을 사용한다는 설명을 접했다.

그런데 Chromium 코드를 읽다 보면 헤더 파일 안에 inline 함수의 구현이 들어 있는 경우가 보인다. 함수 구현을 헤더에 작성해도 괜찮은 걸까? 이 질문을 이해하려면 먼저 C++이 여러 소스 파일을 하나의 프로그램으로 만드는 과정을 알아야 한다.

이 글은 변수, 함수, return 등 C의 기본 문법을 알고 C++을 배우기 시작한 독자를 대상으로 한다. 예제는 macOS에서 clang++를 사용할 수 있는 환경을 기준으로 한다. 아직 컴파일러를 설치하지 않았다면 먼저 코드를 읽으며 흐름을 따라가도 된다.

읽고 나면 다음 세 가지를 설명할 수 있는 것이 목표다.

  • 여러 소스 파일이 하나의 실행 파일이 되는 과정
  • 헤더에 한 번 작성한 함수가 중복 정의될 수 있는 이유
  • inline이 이 문제를 해결하는 방식과 성능 최적화의 차이

먼저, C++ 코드는 어떻게 실행 파일이 될까?

우리가 작성하는 .cc나 .cpp 파일은 C++ 문법으로 된 텍스트다. CPU가 이 텍스트를 그대로 실행하는 것은 아니다. 대상 CPU가 실행할 수 있는 명령으로 바꾸고, 운영체제가 실행할 수 있는 파일 형태로 만드는 과정이 필요하다.

이 글에서는 소스 파일에서 실행 파일을 만드는 전체 과정을 빌드라고 부른다. 일상적으로 “컴파일한다”라고 표현하기도 하지만, 오류를 이해할 때는 컴파일과 링크를 구분하는 편이 좋다.

가장 작은 예제로 시작해 보자. 아래 코드를 hello.cc에 저장한다.

#include <iostream>

int main() {
    std::cout << "Hello, C++!\n";
}

터미널에서 다음 명령을 실행하면 hello라는 실행 파일이 만들어진다.

clang++ -std=c++17 hello.cc -o hello
./hello

출력은 다음과 같다.

Hello, C++!

clang++는 C++ 빌드에 사용하는 Clang 명령이다. -std=c++17은 사용할 언어 표준을, -o hello는 출력 파일의 이름을 지정한다. ./hello는 현재 디렉터리에 만들어진 실행 파일을 실행하는 명령이다.

명령은 한 줄이지만, 이 글에서는 내부 과정을 다음 세 단계로 나누어 이해하자.

소스 파일 hello.cc
   ↓ ① 전처리: 포함할 헤더와 조건부 코드를 처리한다
컴파일할 코드
   ↓ ② 컴파일: 목적 파일을 만든다
목적 파일 hello.o
   ↓ ③ 링크: 필요한 코드와 연결한다
실행 파일 hello

전처리는 컴파일할 내용을 준비하는 단계다. 예제의 #include <iostream>은 헤더의 내용을 포함하라는 지시문이다. 뒤에서 직접 만드는 헤더도 같은 방식으로 소스 파일에 포함된다.

컴파일은 코드의 문법과 타입 등을 검사하고, CPU가 실행할 수 있는 명령으로 바꾸는 과정이다. 여기서는 어셈블 단계를 포함해 목적 파일을 만드는 과정으로 설명한다.

**목적 파일(object file)**은 컴파일한 코드가 담긴 중간 결과물이다. 다른 파일에 있는 함수와의 연결이 아직 남아 있을 수 있으므로 보통 그 자체를 실행하지는 않는다.

링크는 목적 파일과 필요한 라이브러리를 연결하는 단계다. 예를 들어 한 파일에서 호출한 함수의 구현이 다른 파일에 있다면, 이 둘을 연결해 실행 파일을 만든다.

이 글에서 기억할 핵심은 각 소스 파일을 따로 컴파일하고, 그 결과를 링크한다는 점이다.

main.cc → main.o ─┐
                 ├→ 링크 → 실행 파일
add.cc  → add.o  ─┘

컴파일한 결과가 .o 파일이고, 이 파일들을 연결한 결과가 실행 파일이라고 생각하면 이후 예제를 따라갈 수 있다. 세부 단계와 옵션은 Clang 공식 문서에서 확인할 수 있다.

.h와 .cc는 어떤 역할을 할까?

.cc와 .cpp는 모두 C++ 소스 파일에 흔히 사용하는 확장자다. 이 글에서는 Chromium 코드에서 자주 보이는 .cc로 통일한다.

.h는 여러 소스 파일에서 공유할 내용을 담는 헤더 파일에 사용하는 확장자다. 이 글에서 다루는 일반적인 #include 방식에서는 헤더를 별도의 목적 파일로 만드는 대신, 소스 파일에 포함해서 처리한다.

그래서 다음 구조에서 main.cc와 add.cc는 각각 컴파일되지만, add.h의 내용은 두 파일에 포함된다.

main.cc + add.h의 내용 → main.o
add.cc  + add.h의 내용 → add.o

소스 파일에 포함된 헤더 등의 내용을 합치고 전처리를 마친 컴파일 단위를 **번역 단위(translation unit)**라고 한다. 이후 글에서 “각 파일을 따로 컴파일한다”는 표현은 이런 번역 단위를 따로 처리한다는 의미다.

헤더에 어떤 내용을 작성했느냐에 따라, 그 내용이 여러 번역 단위에 들어갈 수 있다. 이제 함수의 선언과 정의를 구분하고, 함수 본문을 헤더에 넣으면 어떤 일이 생기는지 살펴보자.

선언과 정의

다음 코드는 함수의 선언이다.

int Add(int a, int b);

Add라는 함수가 정수 두 개를 받아 정수를 반환한다는 사실을 알린다.

함수 본문까지 작성하면 정의가 된다.

int Add(int a, int b) {
    return a + b;
}

함수를 호출하는 코드를 컴파일할 때는 인자의 개수와 타입, 반환 타입을 알아야 한다. 선언은 이 정보를 제공한다. 함수 본문이 다른 .cc 파일에 있어도, 선언을 알고 있다면 호출하는 쪽의 코드를 컴파일할 수 있다. 실제 함수 구현과 연결하는 작업은 앞에서 살펴본 링크 단계에서 이루어진다. 다만 헤더에 정의가 필요한 inline 함수 등은 뒤에서 구분해 살펴본다.

정의도 선언의 역할을 함께 한다. 따라서 함수 정의가 호출보다 앞에 있다면 별도의 선언을 또 작성할 필요는 없다.

일반적으로 헤더에 선언을 작성하고, .cc 파일에 정의를 작성하는 구조를 많이 본다. 이번에는 일부러 헤더에 정의를 작성해 보자.

헤더에 함수를 정의하고 두 소스 파일에서 포함하기

앞의 hello.cc 예제와는 별도로, 빈 디렉터리에 다음 세 파일을 준비한다. 이후 빌드 명령은 이 디렉터리에서 실행한다.

inline-example/
├── add.h
├── a.cc
└── main.cc

a.cc와 main.cc가 같은 헤더를 사용하는 상황을 만들기 위한 구성이다.

add.h

#ifndef ADD_H_
#define ADD_H_

int Add(int a, int b) {
    return a + b;
}

#endif

a.cc

#include "add.h"

int FromA() {
    return Add(1, 2);
}

main.cc

#include "add.h"
#include <iostream>

int FromA();

int main() {
    std::cout << FromA() << " " << Add(3, 4) << "\n";
}

FromA()는 a.cc에서 Add를 호출하도록 만든 작은 함수다. main.cc에서도 Add를 직접 호출하므로, 두 소스 파일 모두 add.h가 필요하다. main.cc에 적은 int FromA();는 다른 파일에 정의된 함수를 호출하기 위한 선언이다.

세 파일을 같은 디렉터리에 저장하고 다음 명령으로 빌드한다.

clang++ -std=c++17 -O0 a.cc main.cc -o demo

-O0는 최적화 수준을 낮추는 옵션이다. 여기서는 실행 속도보다 함수 정의와 링크의 관계를 관찰하기 위해 사용한다.

macOS의 Clang으로 실행했을 때 다음 링크 오류가 발생했다.

duplicate symbol 'Add(int, int)'

Add는 헤더에 한 번만 작성했는데, 왜 중복되었다고 할까?

컴파일러는 두 소스 파일을 각각 처리한다

앞에서 살펴본 빌드 과정을 이 예제에 적용해 보자. 전처리를 거치면 a.cc와 main.cc 양쪽에 Add의 정의가 들어간다.

a.cc    + add.h의 내용 → a.o
main.cc + add.h의 내용 → main.o

a.o + main.o → 링크 오류

이 예제에서는 a.cc에서 만들어지는 번역 단위와 main.cc에서 만들어지는 번역 단위가 따로 있다. add.h를 독립적으로 컴파일하는 것이 아니라, 헤더의 내용이 각 번역 단위에 들어가는 구조다.

전처리된 내용을 직접 보고 싶다면 다음 명령을 실행할 수 있다.

clang++ -std=c++17 -E a.cc

출력에는 파일 위치를 나타내는 정보와 함께 Add의 본문, FromA의 본문이 나타난다. main.cc도 전처리하면 Add의 본문이 들어 있는 것을 확인할 수 있다. 다만 <iostream>의 내용까지 포함되므로 출력이 훨씬 길다.

이 예제에서는 두 번역 단위가 각각 Add의 정의를 갖는다. 개별 컴파일은 가능하지만, 만들어진 목적 파일들을 링커가 합치는 과정에서 중복된 함수 정의를 발견한다.

오류가 발생하는 단계를 분리해서 확인할 수도 있다.

clang++ -std=c++17 -O0 -c a.cc -o a.o
clang++ -std=c++17 -O0 -c main.cc -o main.o
clang++ a.o main.o -o demo

앞의 두 명령은 성공하고 마지막 링크 명령에서 실패한다. 각 번역 단위만 보면 Add의 정의는 하나씩 있기 때문이다. 두 목적 파일을 합치면서 같은 함수의 정의가 둘이라는 사실이 드러난다.

이 예제의 Add는 다른 소스 파일에서도 같은 이름으로 사용할 수 있는 함수다. 이런 연결 방식을 정확한 용어로 **외부 연결(external linkage)**이라고 한다. 지금은 두 파일에서 같은 Add를 사용하고 있다는 점에 집중하자.

이처럼 외부 연결을 갖는 일반적인 비인라인 함수는 프로그램 전체에 정의를 여러 개 둘 수 없다. 함수의 정의가 어디에 몇 개 있을 수 있는지를 정하는 C++의 규칙을 **단일 정의 규칙(One Definition Rule, ODR)**이라고 한다. C++ 표준 초안의 ODR 규정

헤더 가드로 해결되지 않는 이유

add.h에는 이미 헤더 가드가 있다.

#ifndef ADD_H_
#define ADD_H_
// ...
#endif

첫 번째로 헤더를 포함하면 ADD_H_가 정의된다. 같은 번역 단위에서 이 헤더를 다시 포함하면 #ifndef ADD_H_ 조건을 만족하지 않으므로 본문을 건너뛴다.

헤더 가드는 하나의 번역 단위 안에서 같은 헤더의 내용이 반복해서 포함되는 것을 막는다.

a.cc와 main.cc는 별도로 전처리되므로, 각각 add.h를 한 번씩 포함한다. a.cc를 처리하면서 정의한 ADD_H_가 main.cc를 처리할 때 그대로 이어지는 것은 아니다. 따라서 각 번역 단위에는 여전히 Add의 정의가 하나씩 들어간다.

헤더 가드가 프로그램 전체에서 헤더를 딱 한 번만 포함하도록 만드는 것은 아니다. 여기서 구분해야 할 것은 같은 번역 단위 안의 중복 포함과 서로 다른 번역 단위에 들어간 중복 정의다.

해결 방법 하나: 정의를 .cc 파일로 옮긴다

먼저 가장 일반적인 방법을 적용해 보자. a.cc와 main.cc는 그대로 두고, add.h를 수정한 다음 add.cc를 새로 만든다.

inline-example/
├── add.h    ← 선언만 남긴다
├── add.cc   ← 함수 본문을 이곳으로 옮긴다
├── a.cc     ← 그대로 사용한다
└── main.cc  ← 그대로 사용한다

헤더에는 선언만 남긴다.

// add.h
#ifndef ADD_H_
#define ADD_H_

int Add(int a, int b);

#endif

정의는 별도의 파일에 작성한다.

// add.cc
#include "add.h"

int Add(int a, int b) {
    return a + b;
}

이 방법에서는 아래 세 .cc 파일을 함께 빌드한다. 헤더는 각 소스 파일이 #include하므로 명령에 따로 적지 않는다.

clang++ -std=c++17 -O0 a.cc main.cc add.cc -o demo

a.cc와 main.cc는 선언을 보고 함수를 호출하고, 함수의 정의는 add.cc에서 제공한다.

a.cc    + add.h의 선언 → a.o    ─┐
main.cc + add.h의 선언 → main.o ─┼→ 실행 파일
add.cc  + add.h의 선언 → add.o  ─┘
                        ↑
                 Add의 정의를 제공

선언이 여러 번역 단위에 들어가는 것은 이 예제에서 문제가 되지 않는다. 함수 본문이 add.cc 한 곳에만 있기 때문이다.

반대로 빌드 명령에서 add.cc를 빼면 호출할 함수의 정의를 찾지 못해 링크 오류가 발생한다. 처음에는 정의가 두 개여서 문제였고, 이번에는 필요한 정의가 빠져서 문제가 되는 것이다.

다른 해결 방법: 헤더의 함수에 inline을 붙인다

이번에는 앞 방법에 이어 코드를 더 추가하는 것이 아니라, 처음 예제에서 다른 해결 방법을 적용한다. a.cc와 main.cc는 그대로 두고, add.h를 아래 코드로 바꾼다.

inline-example/
├── add.h    ← inline을 붙인 함수 본문을 둔다
├── a.cc     ← 그대로 사용한다
└── main.cc  ← 그대로 사용한다

앞 단계에서 만든 add.cc는 이 방법에서 사용하지 않는다. 비교를 위해 파일을 남겨 두더라도 아래 빌드 명령에는 포함하지 않는다.

#ifndef ADD_H_
#define ADD_H_

inline int Add(int a, int b) {
    return a + b;
}

#endif

이 방식에서는 add.cc를 빌드에 포함하지 않는다. 원래 명령으로 다시 빌드하면 성공한다.

clang++ -std=c++17 -O0 a.cc main.cc -o demo
./demo

출력은 다음과 같다.

3 7

inline 함수는 정해진 조건을 만족하면 여러 번역 단위에 정의될 수 있다. 이때 하나의 함수로 취급된다. 헤더에 정의를 작성하고 여러 소스 파일에서 포함하는 방식이 가능한 이유다. C++ 표준 초안의 inline 규정

다만 inline을 붙였다고 해서 서로 다른 구현을 작성해도 되는 것은 아니다. 이 예제처럼 같은 함수 정의를 헤더에 두고 여러 소스 파일에서 함께 사용하는 구조로 이해하자.

또한 같은 번역 단위 안에서 정의를 반복하는 것은 여전히 허용되지 않는다. 그래서 inline을 사용한 헤더에도 헤더 가드가 필요하다.

함수 호출 비용은 어떻게 되는 걸까?

일반적인 함수 호출에서는 인자를 전달하고, 함수로 제어를 옮기고, 실행을 마친 뒤 돌아오는 과정이 필요하다. 아주 짧은 함수를 자주 호출한다면 이런 호출 과정의 비용도 고려할 수 있다.

인라인 확장은 함수 호출 지점에 함수 본문에 해당하는 코드를 넣는 최적화다. 개념적으로는 다음과 같은 변화로 이해할 수 있다.

int result = Add(x, y);
int result = x + y;

이는 기계어가 반드시 위 형태로 만들어진다는 뜻이 아니라, 함수 호출을 본문에 해당하는 연산으로 대체한다는 설명용 예시다.

하지만 inline을 붙였다고 해서 컴파일러가 반드시 인라인 확장을 수행해야 하는 것은 아니다. 표준에서도 실제 확장은 필수가 아니며, 확장하지 않아도 inline 함수에 적용되는 규칙은 지켜야 한다고 설명한다. inline과 인라인 확장

반대로 inline이 없는 함수도 컴파일러가 최적화 과정에서 인라인 확장할 수 있다. 실제 확장 여부는 함수 본문, 최적화 옵션, 호출 지점 등 여러 조건의 영향을 받는다.

inline을 붙인 뒤 빌드에 성공했다는 결과는 여러 정의가 허용되는 규칙을 확인한 것이며, 성능 향상을 측정한 결과는 아니다.

따라서 코드에서 inline을 발견했을 때, 그것만으로 실행 속도를 판단할 수는 없다. 헤더에 함수 정의가 들어 있다면 여러 번역 단위에서 그 정의를 공유하는 구조도 함께 살펴봐야 한다.

Chromium 코드에서 찾아보기

이제 실제 코드에 적용해 보자. 확인한 Chromium의 base/logging.h에도 이런 형태의 짧은 함수가 있다.

아래에는 아직 배우지 않은 타입과 문법이 나온다. const LoggingSettings&의 상세 의미는 이후 참조를 공부할 때 살펴봐도 된다. 여기서는 함수 본문이 헤더에 있고, inline이 붙어 있다는 점만 확인하면 된다.

inline bool InitLogging(const LoggingSettings& settings) {
  return BaseInitLoggingImpl(settings);
}

본문은 BaseInitLoggingImpl에 호출을 전달한다. 같은 헤더에는 이 함수의 선언도 있다.

BASE_EXPORT bool BaseInitLoggingImpl(const LoggingSettings& settings);

두 코드를 비교하면 차이가 드러난다. InitLogging은 이 헤더에 정의가 있고, BaseInitLoggingImpl은 이 부분에 선언만 있다. BASE_EXPORT는 Chromium의 심볼 공개와 관련된 매크로이며, 여기서는 뒤에 함수 본문이 있는지에 집중하면 된다.

이 헤더가 여러 번역 단위에 포함되면 InitLogging의 정의도 각 번역 단위에 들어간다. 앞에서 살펴본 inline의 규칙을 적용해 읽을 수 있는 실제 사례다. 다만 이 코드만으로 실제 인라인 확장이 이루어졌는지, 얼마나 빨라졌는지까지 알 수는 없다.

이제 이런 코드를 읽을 때, 함수 본문뿐 아니라 정의가 헤더에 있다는 사실과 inline이 허용하는 규칙도 함께 이해할 수 있다.

Chromium의 가이드에서는 어떻게 권장할까?

앞의 예제에서는 헤더의 함수에 inline을 붙여 링크 오류를 해결했다. 그렇다면 Chromium에서도 함수를 헤더에 작성하고 inline을 붙이는 방식을 널리 사용하면 될까?

이 질문에는 언어가 허용하는 조건과 프로젝트가 권장하는 작성 방식을 나누어 답해야 한다. C++의 규칙은 여러 정의가 있어도 올바른 프로그램인지 판단하는 기준이다. 스타일 가이드는 올바른 코드 중 어떤 형태를 선택해 작성할지 안내한다.

먼저 Chromium 자체 지침을 확인한다

Chromium C++ 스타일 가이드는 별도로 명시한 예외를 제외하고 Google C++ 스타일 가이드를 따른다고 설명한다. 따라서 Google의 설명만 읽고 결론 내리기보다, Chromium에 별도 지침이 있는지 먼저 확인해야 한다.

inline에는 Chromium의 Inline functions 항목이 있다.

Simple accessors should generally be the only inline functions.

대체로 인라인 함수는 단순한 접근자로 제한하라는 뜻이다. 접근자(accessor)는 객체에 저장된 값을 읽어 반환하는 함수 등을 말한다. 예를 들어 저장된 개수 값을 계산이나 반복 없이 그대로 반환하는 함수가 이에 해당한다. 클래스 문법은 이후 장에서 배우겠지만, 지금은 함수가 하는 일이 매우 단순한 경우를 떠올리면 된다.

가이드는 이런 함수의 이름을 snake_case()로 작성하라고도 설명한다. snake_case는 item_count()처럼 단어를 밑줄로 구분하는 이름 표기다. 가상 함수에 대해서는 이런 방식으로 선언하지 말라는 별도 제한도 있다. 가상 함수의 상세 내용은 상속을 배울 때 살펴보고, 여기서는 인라인 함수에 프로젝트 차원의 제한이 있다는 점을 기억하자.

원문의 generally는 일반적인 원칙을 나타낸다. 이를 모든 예외를 금지하는 문장으로 바꾸어 읽어서도 안 되지만, “본문이 한두 줄이면 어떤 함수든 괜찮다”는 뜻으로 넓혀 읽어서도 안 된다.

헤더에는 함수를 사용하는 데 필요한 정보가 먼저 보여야 한다

Google 가이드의 Defining Functions in Header Files 항목은 함수의 공개 선언 위치에 본문까지 함께 작성하는 경우 짧은 함수로 제한하도록 권장한다. 대략 10줄 이하를 예로 들고, 긴 본문은 성능상 또는 기술상 이유로 헤더에 있어야 하는 경우가 아니라면 .cc에 두도록 설명한다.

이 지침은 코드를 읽는 사람의 부담과 연결된다. 다른 파일에서 Add를 사용하려는 사람은 우선 아래 정보가 필요하다.

int Add(int a, int b);

어떤 인자를 받으며 무엇을 반환하는지 확인할 수 있다. 이 함수가 복잡한 검증이나 반복을 수행한다고 가정해 보자. 그 구현이 선언 위치에 길게 들어 있으면, 헤더에서 사용 가능한 함수들을 훑어보기가 어려워진다. Google 가이드는 이런 가독성 문제와 구현 세부 사항의 노출을 단점으로 설명한다.

또한 구현이 헤더에 있어야 한다는 사실과, 공개 선언 위치에 본문을 함께 보여줘야 한다는 판단은 별개라고 설명한다. 헤더에 구현이 필요해도 선언과 구현을 구분해 배치할 수 있다는 뜻이다. 구체적인 배치 방법은 이후 클래스와 템플릿을 배운 뒤 살펴봐도 된다.

여기서 “10줄 이하”는 Google 가이드가 제시한 대략적인 기준이다. Chromium에서 임의의 함수를 인라인으로 작성해도 된다는 허용 기준으로 받아들이면 안 된다. Chromium에는 앞서 확인한 단순한 접근자 중심의 자체 지침이 있기 때문이다.

이번 예제에 적용하면 어떤 선택을 할까?

이 글의 Add는 inline의 언어 규칙을 확인하기 위한 학습 예제다. 빌드 오류가 사라졌다는 사실만으로 Chromium 코드 작성 방식까지 결정되는 것은 아니다.

이번에 다룬 것처럼 여러 파일에서 사용하는 일반 함수를 작성하고, 구현을 헤더에 둘 특별한 이유가 없다면 헤더에 선언을 두고 .cc에 정의하는 방법부터 검토할 수 있다. 단순한 접근자 등 헤더에 정의할 이유가 있는 경우에는 관련 지침을 확인하고, 여러 번역 단위에 정의가 들어가도 ODR을 지키는지 살펴본다. 이는 위 지침을 이번 예제에 적용한 판단 순서다.

앞에서 본 InitLogging은 단순한 접근자가 아니라 다른 함수에 호출을 전달하는 함수다. 이 사례는 헤더 안의 inline 정의를 읽는 데 도움이 되지만, 왜 이 형태를 선택했는지는 해당 코드의 맥락과 변경 이력을 별도로 확인해야 한다. 이 글에서는 그 선택의 이유까지 확인한 것은 아니다.

실제 Chromium 코드를 읽을 때는 현재 코드의 동작을 이해하는 일과 새 코드에 적용할 작성 원칙을 확인하는 일을 함께 하되, 기존 사례 하나를 일반적인 권장 사항으로 확대하지 않는 것이 좋다.

핵심 요약

처음의 질문으로 돌아가 보자. 헤더에 함수를 정의해도 될까? 이 글의 Add처럼 여러 파일에서 함께 사용하는 함수라면, 헤더를 포함한 각 번역 단위에 정의가 들어간다는 점을 고려해야 한다.

  • .cc 파일은 포함된 헤더의 내용과 함께 각각 컴파일되고, 그 결과인 목적 파일들은 링크 단계에서 연결된다.
  • 일반적인 비인라인 함수는 헤더에 선언을 두고 .cc 한 곳에 정의하면 여러 파일에서 사용할 수 있다.
  • 헤더에 함수 정의를 두려면 inline 등을 통해 여러 번역 단위의 정의가 허용되는 조건을 만족해야 한다. 이번 예제에서는 같은 정의를 담은 헤더에 inline을 붙여 해결했다.

주의할 점

  1. 헤더 가드는 프로그램 전체의 중복 정의를 막지 않는다. 같은 번역 단위 안에서 헤더가 반복해서 포함되는 것을 막는다.
  2. inline은 실제 인라인 확장이나 성능 향상을 보장하지 않는다. 여러 정의를 허용하는 언어 규칙과 컴파일러의 최적화 여부를 구분해서 읽어야 한다.
  3. inline을 붙여도 정의가 달라지면 안 된다. 번역 단위마다 매크로에 의해 본문이 달라지는 경우 등을 주의해야 한다. 이런 규칙 위반이 반드시 빌드 오류로 드러나는 것도 아니다.
  4. inline도 같은 번역 단위 안의 중복 정의는 허용하지 않는다. 헤더에 정의를 작성할 때는 헤더 가드도 계속 필요하다.
  5. 언어에서 허용되는 코드와 프로젝트에서 권장하는 코드는 구분한다. Chromium은 대체로 단순한 접근자에 인라인 함수를 제한하는 자체 지침을 둔다.

링크 오류를 만났다면 먼저 필요한 함수 정의가 빠졌는지, 같은 함수의 정의가 여러 목적 파일에 들어갔는지 확인해 보자. 이 글의 예제에서는 그 차이가 각각 정의를 찾지 못하는 오류와 중복 심볼 오류로 나타났다.

더 알아보기: inline 함수의 정의가 달라지면?

여기부터는 첫 독해에서 건너뛰어도 되는 보충 설명이다.

본문에서는 같은 헤더에 같은 함수 정의를 두었다. 하지만 매크로를 사용하면 같은 헤더에서도 서로 다른 코드가 만들어질 수 있다.

inline int Add(int a, int b) {
#ifdef ADD_EXTRA_ONE
    return a + b + 1;
#else
    return a + b;
#endif
}

#ifdef는 지정한 매크로가 정의되어 있는지에 따라 포함할 코드를 고른다. 한 번역 단위에서만 ADD_EXTRA_ONE이 정의되어 있다면, 한쪽의 Add는 a + b + 1을 반환하고 다른 쪽은 a + b를 반환하게 된다.

같은 함수를 사용한다고 하면서 서로 다른 본문을 제공한 셈이다. 이는 여러 정의를 허용하는 조건을 위반하며, 반드시 링크 오류가 나는 것도 아니다. 따라서 빌드에 성공했다는 이유만으로 올바른 코드라고 판단해서는 안 된다.

표준의 정확한 조건에는 각 정의의 토큰열이 같아야 한다는 조건 등이 있다. 토큰은 코드를 구성하는 이름, 연산자 같은 문법 요소다. 본문이 같아 보여도 그 안의 이름이 가리키는 대상이 달라지는 경우 등 추가 조건도 확인해야 한다. 상세 규칙은 C++ 표준 초안의 ODR 항목을 참고할 수 있다.

또한 이 글에서 다룬 외부 연결의 inline 함수는 여러 번역 단위에 정의가 있어도 하나의 함수로 취급되며, 주소도 하나다. 각 소스 파일에 별개의 함수를 만드는 방식과는 구분해야 한다. C++ 표준 초안의 inline 항목