[C++] 인라인 함수와 매크로 함수
인라인 함수와 매크로 함수
인라인 함수와 매크로 함수는 함수 호출에 따른 오버헤드를 피하기 위해 사용합니다.
오버헤드란 프로그램의 실행흐름 도중에 동떨어진 위치의 코드를 실행시켜야 할 때, 추가적인 시간과 메모리 등의 자원이 사용되는 현상입니다.
특히 외부 함수를 사용할 때 함수를 사용하기 위해 스택메모리를 할당하며, 매개변수가 있다면 대입 연산까지도 일어나는 등 많은 과정을 진행하게 됩니다.
이런 오버헤드를 줄이고자 매크로 함수와 인라인 함수를 사용해 최적화하기도 합니다.
오버헤드를 줄이기 위한 목적으로 매크로 함수를 사용하기도 하지만 함수가 아니기 때문에 논리적 오류를 발생시킬 수 있고, 매개변수 형식을 지정할 수 없기 때문에 일반적으로 인라인 함수를 사용합니다.
인라인 함수
C++에서 인라인 함수(Inline Function)는 함수 헤더 앞에 inline
지시자를 붙인 경우로 컴파일러가 인라인 함수를 호출하는 곳에 함수 몸체(function body)를 포함시켜 컴파일을 합니다.
즉 함수 호출이 함수 자체의 코드의 복사본으로 대체됩니다.
인라인 함수를 정의하는 방법은 아래와 같습니다.
inline void PrintHello(); // 전방 선언
inline void PrintHello()
{
std::cout << "Hello!" << std::endl;
}
요즘의 컴파일러는 자동으로 판단하여 짧은 함수를 인라인화 시킵니다.
반대로 너무 긴 함수를 인라인화 하려하면 컴파일러가 무시할 수 도 있습니다.
따라서 대부분의 경우 굳이 사용할 필요는 없습니다.
인라인 함수를 사용하면 다음과같은 오버헤드를 줄입니다.
- 함수 호출(개체의 주소를 스택에 전달 및 배치하는 매개 변수 포함)
- 호출자의 스택 프레임의 보존
- 새 스택 프레임 설정
- 반환 값 전달
- 이전 스택 프레임 복원
- Return
인라인 함수는 함수 오버헤드가 제거되어 실행 속도가 더 빠를 수 있고, 함수의 장점을 가지고 있다는 장점을 가지고 있습니다.
하지만 크기가 크거나 복잡한 태스크를 수행하는 함수의 경우 함수 호출의 오버헤드가 함수의 실행 시간에 비교해 크지 않을 수 있습니다.
그리고 인라인 함수 호출문이 함수의 내용으로 대체되기 때문에 실행 파일의 크기가 커집니다.
인라인 함수는 다음과 같은 상황에서 사용할 수 없습니다.
- 재귀함수
- 재귀호출은 함수의 호출 스택을 통해 구현되지만 인라인 함수는 단순하게 코드가 삽입 될 뿐이므로 사용 할 수 없습니다.
- 함수 포인터로 인라인 함수를 받는 경우
- 인라인 함수는 메모리의 영역이 일반 함수와 다르므로 주소를 받아 사용할 수 있는 함수가 아닙니다. 그렇기 때문에 사용 할 수 없습니다.
위의 경우에 inline
지시자를 붙여 사용한 경우 컴파일러가 인라인화를 하지 않고, 지시자를 무시합니다.
매크로 함수
C언어에서는 C++의 인라인 함수와 비슷한 기능의 매크로 함수(Macro Function)를 사용합니다.
#define
선행처리 지시문에 인수로 함수의 정의를 전달해 함수처럼 동작하는 매크로를 만들 수 있습니다.
매크로 함수는 단순 치환만 해주므로 일반 함수와 똑같은 방식으로 동작하지 않습니다.
매크로 함수를 정의하는 방법은 아래와 같습니다.
#define 매크로이름(매개변수)
#include <iostream>
#define PRINT_NUM(x) std::cout << x
int main() {
PRINT_NUM(30);
}
매크로는 항상 인라인으로 확장됩니다.
그렇기 때문에 매크로는 예기치 않은 동작을 발생시킬 수 있고, 이로 인해 미묘한 버그가 발생할 수 있습니다.
예를 들면
#include <iostream>
#define mult1(a, b) a * b
#define mult2(a, b) (a) * (b)
#define mult3(a, b) ((a) * (b))
int main()
{
std::cout << mult1(2 + 2, 3 + 3) << std::endl; // 11
std::cout << mult2(2 + 2, 3 + 3) << std::endl; // 24
std::cout << mult3(2 + 2, 3 + 3) << std::endl; // 24
}
mult1(2 + 2, 3 + 3)
의 출력 결과를 예상해보면 24가 나와야 합니다.
하지만 11이 출력되는데, 그 이유는 2 + 2 * 3 + 3
로 치환되기 때문입니다.
사칙연산의 계산 순서에 의해 2 * 3
이 먼저 되고, 이후에 2
와 3
이 더해져 11이 된 것입니다.
덧셈을 한 후 곱셈을 하기 위해서는 mult2
의 인자에 해당하는 부분에 소괄호()
를 사용해 각각의 매개변수를 구분해주어야 합니다.
이렇게 하면 당장 mult1
의 문제는 해결 할 수 있지만 더 큰 식의 일부를 mult2
로 사용한다하면 의도와 다른 동작이 발생할 수 있고, 이 문제는 같은 방법을 사용해 mult3
와 같은 방법으로 해결할 수 있습니다.
또 다른 문제로 매크로에 인수로 전달된 식은 경우에 따라 여러번 계산될 수 있습니다.
예를 들면
#include <iostream>
#define sqr(a) ((a) * (a))
int increment(int& number)
{
return number++;
}
int main()
{
int c = 5;
std::cout << sqr(increment(c)) << std::endl; // 30
std::cout << c << std::endl; // 7
}
sqr(increment(c))
의 출력 결과를 예상해보면 25가 나와야 합니다.
하지만 30이 출력되는데, 그 이유는 ((increment(c)) * (increment(c)))
로 치환되기 때문입니다.
함수 increment
가 두 번 호출된 것인데 첫번째 호출에서 c
는 5이지만 두 번째 호출에서 c
는 6이 되므로 30이 된 것입니다.
댓글남기기