Skip to content

Commit

Permalink
Docs: Add Effective C++ 30
Browse files Browse the repository at this point in the history
  • Loading branch information
fkdl0048 committed Dec 4, 2024
1 parent b200679 commit 5ec4365
Showing 1 changed file with 113 additions and 0 deletions.
113 changes: 113 additions & 0 deletions EffectiveC++/Item30.md
Original file line number Diff line number Diff line change
@@ -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<typename T>
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으로 선언하면 안 된다.

0 comments on commit 5ec4365

Please sign in to comment.