[C/C++] inline 함수의 진실과 오해

3 minute read

바쁜 한국인을 위한 요약

inline의 정확한 의미는 컴파일러에게 “여러 translation unit에서 중복으로 정의되었더라도 모두 동일한 것이므로 그 중에서 아무거나 사용해도 좋다“고 친절하게 알려주는 것이다.

서론

구글에 inline 함수를 검색해보면 거의 이렇게 설명한다.

inline 키워드는 컴파일러에게 함수를 인라인하도록 요청합니다.

그러나 이는 많이 부족한 설명이다.

대부분의 현대 C/C++ 컴파일러는 함수 인라인 여부를 스스로 결정합니다. __forceinline이나 __declspec(noinline)같은 키워드로 강제하지 않는 한, inline 키워드는 함수 인라인 여부에 거의 영향을 미치지 않습니다.

이는 맞는 말이다. 그렇다면 이제 inline 키워드는 전혀 쓸모없는 것일까? 그렇지 않다. inline 키워드의 정확한 의미를 파헤쳐보자.

함수 인라이닝

함수 인라이닝이 무엇인가? 기계어 레벨에서 함수 호출을 제거하고 대신 그 자리에 직접 함수의 정의를 때려박는 작업이다. 이를 통해 함수 호출 오버헤드를 제거함은 물론, 문맥에 따른 고수준의 최적화를 가능하게 해준다. 정말 없어서는 안될 기능이라고 할 수 있다.

함수를 인라이닝 하려면 함수의 정의를 때려박아야 하므로, 당연히 호출하는 함수의 정의를 컴파일 시점에 알아야 한다. 그러나 함수의 선언과 정의를 각각 헤더파일과 소스파일로 분리하면, 헤더를 include하여 사용하는 입장에서는 링크 전까지 함수의 정의를 알 수 없다. 동적 라이브러리는 직접 실행하기 전까지도 알 수 없다. 때문에 함수를 인라이닝 하고자 하는 경우, 헤더파일에 함수의 정의까지 포함시키는 것이 일반적이다.

TMI: 링크 시점 인라이닝이 가능하긴 하다. 동적 라이브러리가 아닌 경우, 링크 시점에 함수의 정의를 알 수 있기 때문이다. 하지만 컴파일 시점 인라이닝보다 어렵고, 특히 문맥 최적화가 어려워진다.

중복 정의

다음과 같은 상황을 생각해보자.

// inline.h
...
int max(int a, int b) { return a > b ? a : b; }
...
// a.cpp
#include "inline.h"
...
{
    ...
    int c = max(a, b);
    ...
}
...
// b.cpp
#include "inline.h"
...
{
    ...
    int c = max(a, b);
    ...
}
...

#include는 단순히 해당 파일의 내용을 그대로 복사+붙여넣기 해줄 뿐이므로 아래처럼 변한다.

// a.cpp
...
int max(int a, int b) { return a > b ? a : b; }
...
{
    ...
    int c = max(a, b);
    ...
}
...
// b.cpp
...
int max(int a, int b) { return a > b ? a : b; }
...
{
    ...
    int c = max(a, b);
    ...
}
...

문제가 보이는가? 컴파일은 되지만, 링크 오류가 발생할 것이다. int max(int, int)가 a.obj b.obj 두 곳에 중복 정의되었기 때문이다. 링커는 두 정의 중에서 어느 것을 사용해야 할지 몰라 혼란에 빠지게 된다.

세 가지 해결 방법이 있다. 인라인을 포기하고 정의를 별개의 소스파일로 분리하거나, static 키워드를 사용하거나, inline 키워드를 사용하면 된다. 첫번째 방법은 우리가 원하는 것이 아니니 논외로 하고, 먼저 static 키워드를 설명하겠다.

static

static은 구글에 검색해보면 다행히 잘 설명되어 있다. 해당 기호의 가시성을 현재 translation unit 내부로 제한하는 것이다. 덕분에 여러 cpp 파일에 동일한 시그니처의 변수/함수가 정의되어도 오류가 나지 않는다. 하지만 위 예제에서 사용하기엔 곤란한 점이 있다.

static 키워드는 가시성을 제한할 뿐 중복을 제거하지는 못한다. 다시말해 int max(int, int)의 정의가 translation unit마다 하나씩 있게 되는 것이다. inline.h에 함수가 수십 개 있고, 이를 include하는 cpp 파일이 수십 개라고 해보자. 그럼 최악의 경우 동일한 코드가 수천 번 중복된다. 프로그램의 크기가 쓸데없이 커질 것이다. 이는 좋은 해결책이 아니다.

inline

#include를 사용할 경우, 해당 파일의 내용이 그대로 복제된다. 다시 말해, 여러 소스파일에서 같은 헤더파일을 #include할 경우, 해당 소스파일들에는 완전히 동일한 내용이 있게 되는 것이다. 그럼 어차피 똑같은 거, 링크할 때 아무거나 하나만 살리고 나머지는 버리면 되지 않을까?

이 역할을 하는 것이 inline이다. inline의 정확한 의미는 컴파일러에게 여러 translation unit에서 중복으로 정의되었더라도 모두 동일한 것이므로 그 중에서 아무거나 사용해도 좋다고 친절하게 알려주는 것이다. 쉽게 말해 ODR을 무시할 수 있도록 해준다. 덕분에 최종 산출물(라이브러리/실행파일)에서 같은 코드가 중복되지 않을 수 있다.

TMI: 클래스 멤버함수를 선언과 동시에 정의하거나, constexpr 함수, template 함수가 암시적으로 inline 함수가 되는 이유를 이제 알 수 있을 것이다. C++의 클래스 정의는 원래 모든 translation unit에서 동일하다는 가정 하에 공유하도록 되어있으므로, 클래스 안에 함수를 정의할 경우 inline 함수가 되는 것이 당연하다. 마찬가지로 constexpr 함수와 template 함수는 컴파일 시점에 함수의 정의를 알아야 의미가 있으므로 inline 함수가 되는 것이 당연하다. (물론 특수화된 template 함수는 해당되지 않는다)

마치며

나무위키도 inline 함수의 이런 의미를 문서의 아래쪽에서 인라이닝의 문법적 의의라며 잘 설명하고 있긴 하다. 필자는 inline 키워드를 설명할 때, 이제 의미가 없어진 ‘컴파일러에게 인라이닝 하도록 요청’ 보다는 ‘ODR을 무시하도록 허용‘하는 것에 더 큰 의의를 두었으면 한다.

Categories: ,

Updated:

Comments