C++: 전역/정적 객체 우선 순위 문제
C++ 프로그래밍을 하다보면 전역/정적 객체가 필요할 때가 있다. 전역/정적 객체의 용도 중의 하나는 생성과 소멸이 자동으로 이루어지는 객체의 속성을 이용해서, 전역/정적 객체를 가지고 프로그램 전체의 초기화 작업과 마무리 작업을 수행하는 것이다.
하지만, 이러한 전역/정적 객체의 골칫거리가 하나 있는데, 바로 전역/정적 객체의 생성/소멸 순서이다. 어느 전역/정적 객체가 먼저 생성되고 나중에 생성될지 알 수 없기 때문에, 한 전역/정적 객체의 생성자에서 다른 전역/정적 객체를 참조하게 되면, 예상치 못한 오류를 만날 수 있다.
그래서 이를 해결하기 위해 여러가지 방법이 나왔다. 우선 컴파일러 차원에서 생성자들의 우선순위를 정할 수 있는 방법을 제공한다. #pragma 를 지시자를 이용해서 지원하는데, 이 역시도 우선 순위가 같은 객체 사이에서는 마찬가지 문제가 발생할 수 있기 때문에 근본적인 해결책은 될 수 없다.
그렇다면 근본적인 해결 방법은 없는 것일까 ? 다행히도, 그렇지는 않다. Effective C++ 항목 4 를 보면 해결 방법이 나온다. 전역/정적 객체 문제의 핵심은 객체가 이미 생성됐다는 것을 보증할 수 없다는 것이다. 하지만 조금만 틀어보면 이것이 불가능한 것은 아니다. 바로 함수 내부에 정적 객체를 선언하는 것이다. 함수 내부의 정적 객체들은 함수가 처음 호출될 때 반드시 한 번만 초기화되기 때문에, 함수를 이용해서 정적 객체에 대한 참조나 포인터를 되돌려 주면, 해당 정적 객체는 반드시 초기화되었다는 보증할 수 있다.
예를 들어, 클래스 A 와 클래스 B 가 각각 a.cpp 와 b.cpp 에 있다고 하자.
만일 클래스 B의 객체를 생성하게 되면 문제가 발생할 수 있다는 것이다. 이를 해결하기 위해서 EC++ 에서 제시한 방법이 다음과 같은 것이다.
이제는 클래스 B 의 객체를 생성하더라도 아무 문제 없이 프로그램이 실행될 것이다. 이로써 초기화 순서 문제를 근본적으로 해결할 수 있다.
그렇다면 모든 문제가 해결되었을까 ? 그렇지는 않다. 초기화 순서는 해결되었을 지도 모르지만, 마무리 순서는 여전히 미해결로 남아있다. 왜 그럴까 ?
이번에는 B 의 소멸자에서 생성자처럼 ao().af() 를 호출한다고 생각해 보자.이렇게 되면, 처음의 문제로 다시 되돌아가게 된다. B 의 소멸자에서 ao().af() 를 호출할 때 ao()의 지역 정적 객체가 이미 소멸되었을 수도 있기 때문이다. 이런 경우 잘못된 결과가 나오거나 예외가 발생하게 될 것이다.
이에 대한 해결방법은 없는 것으로 보인다. 생성 순서와는 달리 소멸 순서는 그 순서를 강제할 방법이 없기 딱히 때문이다. 그렇다면 어떻게 해야 할까 ? 어쩔 수 없다. 전역/정적 객체의 소멸자에서는 전역/정적 객체를 쓰지 말아야 한다.
하지만, 이러한 전역/정적 객체의 골칫거리가 하나 있는데, 바로 전역/정적 객체의 생성/소멸 순서이다. 어느 전역/정적 객체가 먼저 생성되고 나중에 생성될지 알 수 없기 때문에, 한 전역/정적 객체의 생성자에서 다른 전역/정적 객체를 참조하게 되면, 예상치 못한 오류를 만날 수 있다.
그래서 이를 해결하기 위해 여러가지 방법이 나왔다. 우선 컴파일러 차원에서 생성자들의 우선순위를 정할 수 있는 방법을 제공한다. #pragma 를 지시자를 이용해서 지원하는데, 이 역시도 우선 순위가 같은 객체 사이에서는 마찬가지 문제가 발생할 수 있기 때문에 근본적인 해결책은 될 수 없다.
그렇다면 근본적인 해결 방법은 없는 것일까 ? 다행히도, 그렇지는 않다. Effective C++ 항목 4 를 보면 해결 방법이 나온다. 전역/정적 객체 문제의 핵심은 객체가 이미 생성됐다는 것을 보증할 수 없다는 것이다. 하지만 조금만 틀어보면 이것이 불가능한 것은 아니다. 바로 함수 내부에 정적 객체를 선언하는 것이다. 함수 내부의 정적 객체들은 함수가 처음 호출될 때 반드시 한 번만 초기화되기 때문에, 함수를 이용해서 정적 객체에 대한 참조나 포인터를 되돌려 주면, 해당 정적 객체는 반드시 초기화되었다는 보증할 수 있다.
예를 들어, 클래스 A 와 클래스 B 가 각각 a.cpp 와 b.cpp 에 있다고 하자.
1 2 3 4 5 6 7 8 9 | // a.cpp class A { public: A(); ~A() {}; void af(); }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // b.cpp class B { public: B(); ~B() {}; }; extern A a; void B::B() { a.af(); } |
만일 클래스 B의 객체를 생성하게 되면 문제가 발생할 수 있다는 것이다. 이를 해결하기 위해서 EC++ 에서 제시한 방법이 다음과 같은 것이다.
1 2 3 4 5 6 7 8 9 | // a2.cpp class A {...}; // 생략 A& ao() { static A a; return a; } |
1 2 3 4 5 6 7 8 9 | // b2.cpp class B {...}; // 생략 extern A& ao(); void B::B() { ao().af(); } |
이제는 클래스 B 의 객체를 생성하더라도 아무 문제 없이 프로그램이 실행될 것이다. 이로써 초기화 순서 문제를 근본적으로 해결할 수 있다.
그렇다면 모든 문제가 해결되었을까 ? 그렇지는 않다. 초기화 순서는 해결되었을 지도 모르지만, 마무리 순서는 여전히 미해결로 남아있다. 왜 그럴까 ?
이번에는 B 의 소멸자에서 생성자처럼 ao().af() 를 호출한다고 생각해 보자.이렇게 되면, 처음의 문제로 다시 되돌아가게 된다. B 의 소멸자에서 ao().af() 를 호출할 때 ao()의 지역 정적 객체가 이미 소멸되었을 수도 있기 때문이다. 이런 경우 잘못된 결과가 나오거나 예외가 발생하게 될 것이다.
이에 대한 해결방법은 없는 것으로 보인다. 생성 순서와는 달리 소멸 순서는 그 순서를 강제할 방법이 없기 딱히 때문이다. 그렇다면 어떻게 해야 할까 ? 어쩔 수 없다. 전역/정적 객체의 소멸자에서는 전역/정적 객체를 쓰지 말아야 한다.
댓글
댓글 쓰기