[Effective C++]new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자

자원 관리, 다섯 번째 이야기

Posted by SungBeom on December 05, 2019 · 3 mins read

컴파일러의 실행 순서에 따른 자원 누출 가능성

처리 우선순위를 알려 주는 함수가 하나 있고, 동적으로 할당한 Widget 객체에 대해 어떤 우선순위에 따라 처리를 적용하는 함수가 하나 있다고 가정합니다. 자원 관리에는 객체를 사용하여, process Widget 함수는 동적 할당된 Widget 객체에 대해 스마트 포인터(여기서는 tr1::shared_ptr)를 사용하도록 만들어졌습니다. 여기서 포인터를 받는 tr1::shared_ptr의 생성자는 explicit로 선언되어 'new Widget' 표현식에 의해 만들어진 포인터가 tr1::shared_ptr 타입의 객체로 바꾸는 암시적인 변환이 없기 때문에, std::tr1::shared_ptr(new Widget)으로 넘겨주어야 합니다.

1
2
3
4
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int p);
 
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
cs

위 예제에서 자원 관리 객체를 쓰고 있는데도, 마지막 문장은 자원을 흘릴 가능성이 있습니다. 컴파일러는 processWidget 호출 코드를 만들기 전에 우선 이 함수의 매개변수로 넘겨지는 인자를 평가(evaluate)하는 순서를 밟습니다. 여기서 두 번째 인자는 priority 함수의 호출문밖에 없지만, 첫 번째 인자("std::tr1::shared_ptr<Widget>(new Widget)")는 두 부분으로 나누어져 있습니다.

"new Widget" 표현식을 실행하는 부분
tr1::shared_ptr 생성자를 호출하는 부분

사정이 이렇기 때문에, processWidget 함수 호출이 이루어지기 전에 컴파일러는 다음 세 가지 연산을 위한 코드를 만들어야 합니다.

priority를 호출합니다.
"new Widget"을 실행합니다.
tr1::shared_ptr 생성자를 호출합니다.

그런데, 여기서 각각의 연산이 실행되는 순서는 컴파일러 제작사마다 다르다는 게 문제입니다. C++ 컴파일러의 경우엔 이들의 순서를 정하는 데 있어서 상당한 자유도를 갖고 있거든요(자바 및 C# 등의 언어와 다른 점 중 하나가 이 부분인데, 자바 및 C#은 매개변수의 평가 순서가 특정하게 고정되어 있습니다). "new Widget" 표현식은 tr1::shared_ptr 생성자가 실행될 수 있기 전에 호출되어야 하겠지요. 그러나 priority의 호출은 처음에 호출될 수도 있고, 두 번째나 세 번째에 호출될 수도 있습니다.

1. "new Widget"을 실행합니다.
2. priority를 호출합니다.
3. tr1::shared_ptr 생성자를 호출합니다.
위와 같은 순서로 결정됐을 때, priority 호출 부분에서 예외가 발생했다면 "new Widget"으로 만들어졌던 포인터가 유실됩니다. 그러니까 processWidget 호출 중에 자원이 누출될 가능성이 있는 이유는, 자원이 생성되는 시점("new Widget"을 통한)과 그 자원이 자원 관리 객체로 넘어가는 시점 사이에 예외가 끼어들 수 있기 때문입니다.

이런 문제를 피하기 위해선 Widget응 생성해서 스마트 포인터에 저장하는 코드를 별도의 문장 하나로 만들고, 그 스마트 포인터를 processWidget에 넘기는 것입니다.

1
2
3
4
std::tr1::shared_ptr<Widget> pw(new Widget);  // new로 생성한 객체를
                                              // 스마트 포인터에 담는 코드를
                                              // 하나의 독립적인 문장으로 만듭니다.
processWidget(pw, priority());                // 이제는 자원 누출 걱정이 없습니다.
cs

한 문장 안에 있는 연산들보다 문장과 문장 사이에 있는 연산들이 컴파일러의 재조정을 받을 여지가 적기 때문에 위의 코드는 자원 누출 가능성이 없습니다.


정리

new로 생성한 객체를 스마트 포인터에 넣는 코드는 별도의 한 문장으로 만듭시다. 이것이 안 되어 있으면, 예외가 발생될 때 디버깅하기 힘든 자원 누출이 초래될 수 있습니다.