From 5ec4365a3c4a5890214c1d6ec24dfb001ce04d3c Mon Sep 17 00:00:00 2001 From: fkdl0048 Date: Wed, 4 Dec 2024 17:23:30 +0900 Subject: [PATCH] Docs: Add Effective C++ 30 --- EffectiveC++/Item30.md | 113 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 EffectiveC++/Item30.md diff --git a/EffectiveC++/Item30.md b/EffectiveC++/Item30.md new file mode 100644 index 0000000..9aae0af --- /dev/null +++ b/EffectiveC++/Item30.md @@ -0,0 +1,113 @@ +## Item 30: 인라인 함수는 미주알고주알 따져서 이해해 두자 + +인라인 함수에 대한 좋은 점은 [Item2](https://github.com/fkdl0048/BookReview/issues/278)에서 간략하게 다뤘다. 이번 장에서는 Inilne 함수에 대해서 좀 더 자세하게 알아본다. + +단순하게 인라인 함수를 사용하면 본문을 그대로 넣어준다라는 개념으로 이해하기 보다. 대체적으로 컴파일러 최적화는 함수 호출이 없는 코드가 연속적으로 이어지는 구간에 적용되도록 설계되었기 때문에, **인라인 함수를 사용하면 컴파일러가 함수 본문에 대해 문맥별 최적화를 걸기 용이해진다는 개념이 있다.** *아웃라인 함수(일반 함수)는 이런 최적화를 적용하지 않는다.* + +그러나 항상 **트레이드 오프**가 일상인 프로그래밍 세계에서 인라인 함수도 피해갈 수 없다. 인라인 함수는 결국 함수의 호출문을 함수의 본문으로 바꿔치기 하는 것이라 목적 코드의 크기가 커지는 것을 피할 수 없다. + +즉, 메모리가 제한된 컴퓨터에서 아무 생각 없이 남발했다간 프로그램 크기가 그 기계에서 쓸 수 있는 공간을 넘어갈 수 있다. 페이징 횟수가 늘어나고, 명령어 캐시 적중률이 떨어질 가능성이 있다. + +반대의 경우로 본문 길이가 매우 짧은 인라인 함수를 사용하면, 함수 본문에 대해 만들어지는 코드의 크기가 함수 호출문에 대해 만들어지는 코드보다 작아질 수 있다. 이런 경우에는 목적 코드의 크기도 작아지며, 명령어 캐시 적중률도 높아진다. + +결국 inline함수는 컴파일러에 '요청'하는 것이지, '명령'이 아니다. 이 요청은 inline을 붙이지 않아도 암시적으로 되는 경우도 있고, 명시적으로 되는 경우도 있다. + +### 암시적 inline + +```cpp +class Person { +public: + int age() const { return theAge; } +private: + int theAge; +}; +``` + +이런 함수는 암시적으로 inline함수로 선언된다. + +### 명시적 inline + +```cpp +template +inline const T& min(const T& a, const T& b) +{ + return a < b ? a : b; +} +``` + +*여기선 inline함수로 나오지만 C++ 11이상 constexpr을 사용하면 더 좋다.* + +**인라인 함수는 대체적으로 헤더 파일에 들어 있어야 하는게 맞다.** 왜냐하면 대부분의 빌드 환경에서인라인을 컴파일 도중에 수행하기 때문이다. 인라인 함수 호출을 그 함수의 본문으로 바꿔치기 하려면, 일단 컴파일러는 그 함수가 어떤 형태인지 알고 있어야 한다. + +템플릿 역시 헤더파일에 들어 있어야 한다. 템플릿이 사용되는 부분에서 해당 템플릿을 인스턴스로 만들려면 그것이 어떻게 생겼는지를 컴파일러가 알아야 하기 때문이다. + +### inline은 컴파일러 선에서 무시할 수 있는 요청 + +대부분의 컴파일러의 경우 아무리 인라인 함수로 선언되어 있어도 자신이 보기에 복잡한 함수는 절대로 인라인 확장의 대상에 넣지 않는다. *C++ 14부터는 조건문이나 반복문도 inline으로 선언할 수 있다.* + +- `virtual`의 의미가 "어떤 함수를 호출할지 결정하는 작업을 런타임에 한다" +- `inline`의 의미가 "함수 호출 위치에 호출된 함수를 끼워 넣는 작업을 컴파일 타임에 한다" + +결국 인라인 함수가 되냐 안되느냐는 전적으로 개발자가 사용하는 빌드 환경에 달렸다. + +### 함수 포인터와 inline + +어떤 인라인 함수의 주소를 취하는 코드가 있으면, 이 코드를 위해 아웃라인 함수 본문을 만들 수 밖에 없을 것이다. *있지도 않은 함수에 대해 주소를 취하는 것은 불가능하다.* + +```cpp +inline void f() +{ + ... +} + +void (*pf)() = f; +... +f(); // 인라인 + +pf(); // 아웃라인 +``` + +### 생성자와 소멸자 inline + +먼저 말하지만 생성자와 소멸자는 인라인 하기 좋은 함수가 아니다. + +```cpp +class Base { +public: + ... + +private: + std::string bm1, bm2; +}; + +class Derived : public Base { +public: + Derived() {} + ... +private: + std::string dm1, dm2, dm3; +}; +``` + +C++은 객체가 생성되고 소멸될 때 일어나는 일들에 대해 여러 가지 보장을 준비해놨다. new을 통해 동적으로 생성하는 경우 자동으로 생성자를 호출해주거나 delete를 통해 소멸하는 경우 소멸자를 호출해준다. 이런 일들은 컴파일러가 자동으로 처리해주는 것은 C++에 깔아둔 보장이다. + +어떤 객체를 생성하면 그 객체의 기본 클래스 부분과 그 객체의 데이터 멤버들이 자동으로 생성되며, 그 객체가 소멸할 때 이에 반대되는 순서로 소멸 과정이 일어나는 것도 마찬가지다. 또한 생성 과정에서 예외가 발생하더라도 이미 생성된 부분은 말끔하게 소멸자를 호출해주는 것도 마찬가지다. + +**핵심은 C++은 위와 같이 무엇을 해야 하는지는 정해두었지만, 어떻게 해야 하는지는 정해두지 않았다. (컴파일러 구현자에게 달림)** + +Derived 클래스의 생성자는 최소한 자신의 데이터 멤버와 기본 클래스 부분에 대해 생성자를 호출해 주어야 하고, 이들 생성자를 호출해야 하기 때문에 인라인이 적합하지 않다. + +### 정리 + +이처럼 인라인 함수는 생각보다 따질 것이 많다. 개발에서 이를 명확하게 알고 필요시 사용하는 것이 중요하다. getter, setter함수에만 사용하는 것이 좋을 것 같다..! + +- constexpr + - 상수 표현식으로 계산 가능한 함수가 필요할 때. + - 컴파일 시간 최적화와 상수 초기화가 중요한 경우. + - 예: 수학적 계산, 상수 값 생성. +- inline 사용: + - 작은 함수에서 호출 오버헤드를 줄이고 싶을 때. + - 코드 크기가 크지 않은 단순한 유틸리티 함수에 적합. + - 예: 단순한 getter, setter, 연산 함수. +- 함수 인라인은 작고, 자주 호출되는 함수에 대해서만 하는 것으로 묶는다. 이렇게 되면 디버깅 및 라이브러리의 바이너리 업그레이드가 용이해지고, 자칫 생길 수 있는 코드 부풀림 현상을 최소화하며, 프로그램의 속력이 더 빨라질 수 있는 여지가 최고로 많아진다. +- 함수 템플릿이 대개 헤더 파일에 들어간다는 일반적인 부분만 생각해서 이들을 inline으로 선언하면 안 된다.