비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해진다🙃

안녕하세요! 두두코딩 입니다 ✋
오늘은 비지역 정적 객체 초기화 순서에 대해 알아보겠습니다.

이전 포스팅에 이어 포스팅을 진행합니다. 전 포스팅을 읽고 해당 포스팅을 읽을 경우 더욱 더 이해가 쉽습니다!🙃

🖇 소스코드에 마우스를 올리고 copy 버튼을 누를 경우 더 쉽게 복사할 수 있습니다!

궁금한 점, 보안점 남겨주시면 성실히 답변하겠습니다. 😁
+ 감상평 댓글로 남겨주시면 힘이됩니다. 🙇

컴파일러 초기화 순서

해당 블로그에서 컴파일러 순서 관련 포스팅을 진행한 적 있다. Effective C++에서도 컴파일러 초기화 순서에 관해 이야기하고 있다.

C++에서의 객체 초기화는 앞선 포스팅에서 봤듯이 꽤나 번거롭다. 하지만, 변덕스럽지 않은 부분이 있는데, 컴파일러가 객체를 구성하는 데이터 초기화 순서이다.

  1. 기본 클래스는 파생클래스보다 먼저 초기화 된다.
  2. 클래스 데이터 멤버는 그들의 선언된 순서대로 초기화 된다.

1번 같은 경우 관련 포스팅에서도 많이 다뤘다 참고. 2번에 대해서 이야기를 해보자.

ABEntry (앞선 포스팅에서 작성한 클래스 이름..) 클래스로 예를들면 theName이 항상 첫번째로 초기화되고, 두 번째는 thePhone 순으로 초기화 된다. 즉, 선언된 순서와 초기화 순서가 동일하게 일어난다. 선언순서와 초기화 리스트에 들어간 순서가 다르더라도, 문제 없이 컴파일은 되지만, 아마 컴파일러 에러를 만나게 될 것이다.

보통 사람들의 혼돈을 막고, 찾아내기 힘든 동작의 버그를 피하기 위해서는 2번 규칙을 꼭 지키도록 하자 😏

우리는 현재까지 기본타입, 사용자 타입(클래스), 데이터 멤버 등에 대해 초기화 하는 방법을 배웠다. 이제 정적 객체 초기화만 남았고, 같이 알아보도록 하자.

정적객체 초기화

정적객체의 초기화를 알기 위해서는 아래의 문구를 이해해야한다.

❗ 비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해진다는 사실

Tip 번역단위에 대해 먼저 알아보자. 번역단위란 아래의 그림과 같이, 쉽게 말해 컴파일 이전의 모든 헤더파일과 extern file, source code 파일들을 모은 것을 말한다.

unit


정적 객체 (static object)는 생성된 시점부터 프로그램 종료까지 살아있는 객체를 말한다. 따라서, 우리가 알고 있는 메모리 영역인 stack 영역heap 영역에는 해당 객체가 존재할 수 없다.

그럼 어떤 종류를 정적 객체라고 칭할까?

  1. 전역 객체
  2. 네임스페이스 유효범위에서 정의된 객체
  3. 클래스 내 static으로 선언된 객체
  4. 함수 내 static으로 선언된 객체
  5. 파일 유효범위에서 static으로 정의된 객체

위 5가지 종류를 정적 객체라고 칭한다. 해당 종류에서 함수 내에 있냐 없냐로 지역 정적 객체 (local static object)비지역 정적 객체 (non-local static object)로 나뉘게 된다.

위의 정리 내용을 통해 “처음 언급했던 문구”를 다시 이해해보자. 번역단위 별로 초기화가 이뤄진다는 의미는 별개의 번역 단위에서 정의된 비지역 정적 객체들의 초기화 순서는 정해져 있지 않다 라는 의미와 동일하다.

구체적으로, 한쪽 번역 단위에 있는 비정적 객체의 초기화가 진행되면서 다른 쪽 번역 단위에 있는 비정적 객체를 사용한다고 할 경우, 사용되는 쪽에 객체가 초기화가 되어있는지 안되어있는지 알 수 없는 상태에서 프로그램이 실행된다는 의미이다😱

비지역 정적객체 초기화 문제

비지역 정적객체 초기화로 발생하는 문제를 소스코드로 확인해보자.

class FileSystem {
public:
	...
	std::size_t numDisk() const;	// 많고 많은 멤버 중 하나
	...
};

extern FileSystem tfs;	// 사용자가 사용할 객체
						// "tfs" = "the file system"

위의 코드와 같이, 파일들을 관리하는 파일시스템 객체가 있다고 생각해보자. 사용자가 현재 디스크의 개수를 얻으려면 tfs.numDisk()를 활용하여 개수를 얻으면 된다. 즉, tfs라는 정적 객체를 하나 열어준다고 생각하자.

또 다른 클래스를 하나 보자.

class Directory {
public:
	Directory( params );
	...
};

Directory::Directory( params )
{
	...
	std::size_t disks = tfs.numDisk();	// tfs 객체 사용
	...
};

위의 객체는 사용자가 directory를 생성하거나 할 때, 사용하는 객체이다. 사용자가 임시 directory를 얻기위해서는 Directory tempDir( params );와 같이 객체 생성을 통해 얻을 수 있다. 하지만, Directory 생성자 내에서 disks의 개수를 확인해, 현재 공간할당이 가능한지 유무를 체크하는 함수가 있다고 가정해보자.

우리는 이전의 클래스를 통해 disk의 개수를 얻기 위해서는 tfs 객체에 접근을 해야한다. 이 상황에서 우리가 앞에서 계속 이야기했던 “정적 객체의 초기화 순서가 보장되지 않음 (비지역 정적 초기화 문제)” 문제를 만날 수 있다. 즉, tfs 객체의 초기화가 되지 않은 상태에서 Directory 클래스에서 tfs를 사용할 수 있는 경우가 생긴 것이다.

조금 더 자세히 문제를 들여다 보면, 우리는 tempDir를 활용해 Directory를 하나 생성하려 했다. 이 시점에 tfs는 다른 번역 단위 내 존재하고 있고 아직 초기화가 일어나지 않은 상태이다. 하지만, tempDir 번역 단위에 있는 부분에서 tfs를 사용하며, 초기화 되지 않은 값을 기준으로 컴파일 되게 된다. (이럴경우, 나중에 버그로 만나게된다 ㅠㅠ,,😤)

우리는 버그를 피하기 위해 비지역 정적 초기화 문제를 해결해야하는데 어떻게 해결할까?

비지역 정적 초기화 문제 해결방법

비지역 정적 객체들의 초기화에 대해 “적절한” 순서를 결정하기란 정말 어렵다. 컴파일러마다 맞출수도 없는문제고.. 하지만 너무 절망할 필요없다.

우리는 약간의 설계를 변경해 해당 문제를 해결할 수 있다.

❗하나의 함수를 만들고 함수 내 비정적 객체를 넣어라

함수 내에서 비정적 객체를 생성하고 반환하는 함수를 하나 생성해 문제를 해결할 수 있다. 사용자 쪽에서 직접 비정적 객체를 참조하는 것이아니라 함수 호출로 대신 하도록 변경한다. 즉, 우리는 정적 객체 중 비지역 정적 객체 지역 정적 객체로 변환한 것이다.

이 패턴은 “싱글턴 패턴”으로 많이 활용하고 있으며, 해당 디자인 패턴은 추후 다루도록하겠다.

C++에서는 지역 정적 객체는 함수 호출 중에 그 객체의 정의에 최초로 닿았을 때 초기화 하도록 만들어 져있으며, 이를 보장한다.

이 설계를 사용할 경우, 비정적 초기화 문제를 해결하고 지역 객체를 호출할 일이 없다면 생성 / 소멸에 대한 overhead도 줄일 수 있게 된다. (이전에는 호출 유무와 관계없이, 정적객체는 무조건 생성 / 소멸을 했기 때문에..)

그럼 위의 문제 예시를 변경해보자.

class FileSystem { ... };

// 정적 객체
FileSystem& tfs()
{
	static FileSystem fs;
	return fs;
}

class Directory { ... };

Directory::Directory ( params )
{
	...
	std::size_t disks = tfs().numDisks();
}

Directory& tempDir(params)
{
	static Directory td(params);	// "td = tempDir"
	return td;
}

위의 코드를 보면 많이 바뀐 것 같지만, 그렇지 않다. 바꾼 부분은 명확하다. 기존에 tfs 혹은 tempDir를 직접 사용했다면, 이제 tfs() 혹은 tempDir() 함수 호출을 통해 참조하도록 변경하였다.

Appendix

어떤 객체가 초기화 되기 전에 그 객체를 사용하는 일이 생기지 않도록 하려면 3가지를 기억해라.

  1. 멤버가 아닌 기본제공 타입은 직접 초기화 해라
  2. 객체의 모든 부분에 대한 초기화는 초기화 리스트를 활용해라
  3. 별개의 번역 단위에 정의된 비지역 정적 객체에 영향이 있는 부분은 설계를 전환해라. (직접이 아니라 참조반환 함수로 설계해라.)

내가 잘 몰랐던 부분은 잊지말자.

⚔ 여러 번역단위에 있는 비지역 정적 객체들의 초기화 순서 문제는 피해서 설계해야 된다. 비지역 정적 객체를 지역 정적 객체로 바꾸는 것을 습관화 하자.