selectany 전역 변수의 실종

전역 변수의 범위

전역 변수를 선언하면 선언한 오브젝트에만 할당된다. 다른 오브젝트에서 그 전역 변수를 사용하기 위해서는 사용하고자 하는 파일에서 extern 지시자를 이용해야만 한다. 그게 불편하다고 무턱대고 공용 헤더 파일에 선언했다가는 중복 선언이라는 끔찍한 에러 메시지를 보게 된다. (링크 에러 2005)

물론, 한 cpp 파일에 전역 변수를 선언하고, (프리컴파일 헤더 같은) 공용 헤더 파일에 extern 지시자를 써서 선언하면 어렵지 않게 해결할 수 있다.

selectany란?

편리하게도 이런 불편(?)함을 한 방에 해결할 수 있는 지시자가 있는데 이것이 바로 selectany 이다. 이 지시자는 전역변수를 COMDAT(COMDAT에 대해서는 나중에 설명한다)으로 지정한다. 링커는 중복된 COMDAT이 있으면 하나만 남기고 모두 없애버린다. 따라서, 전역 변수가 여러 오브젝트에 걸쳐 중복 선언돼도 링커가 알아서 처리해 준다. 그러므로 그냥 공용 헤더에 선언하기만 하면 된다. 선언은 다음과 같이 한다.

__declspec( selectany ) 선언자;

전역 변수의 실종

하지만, 몇 가지 주의해서 사용해야 한다. 특히, /OPT:REF라는 최적화 옵션이 켜져 있다면, 전역 객체를 선언했음에도 불구하고 예상치 못한 결과를 경험할 수 있다. 프로젝트 설정에 이 옵션이 켜져 있으면, 링커는 참조되지 않는 selectany 전역 변수를 없애버린다. 단순히 볼 때, 이러한 최적화 방식은 아무 문제가 없어 보인다. 하지만 필자는 이와 관련한 문제를 종종 접할 수 있었다. 다음의 코드를 살펴보자.

__declspec(selectany) std::vector<int> gIntegerVector;

class AutoPusher
{
public:
    AutoPusher(int i)
    {
        gIntegerVector.push_back( i );
    }
};

__declspec(selectany) AutoPusher ap1(1);
__declspec(selectany) AutoPusher ap2(2);
__declspec(selectany) AutoPusher ap3(3);

위의 코드는 전역 벡터 gIntegerVector에 자동으로 데이터를 추가하는 클래스 AutoPusher를 구현한다. 그리고 1, 2, 3을 자동으로 등록할 수 있도록 ap1, ap2, ap3를 선언한다. 다음은 전역 벡터를 출력하는 코드이다.

 std::cout << gIntegerVector = [ ";
 std::vector<int>::const_iterator i = gIntegerVector.begin();
 for (; i != gIntegerVector.end(); i++)
 {
    std::cout << *i << " ";
 }
 std::cout << "]" << std::endl;

출력 값은 링크 옵션 /OPT에 따라 다르다. /OPT:NOREF 일 경우에 링커는 데이터가 참조되지 않더라도 그 값을 유지한다. 따라서 출력결과는 우리가 예상하던 대로 다음과 같다.

gIntegerVector = [ 1 2 3 ]

하지만, /OPT:REF 옵션일 경우에는 ap1, ap2, ap3 가 참조되지 않기 때문에 링크할 때 제거된다. 따라서 AutoPusher 클래스의 생성자는 절대 실행되지 않는다. 따라서, 출력 결과는 우리의 예상과 달라진다.

gIntegerVector = [ ]

정적 멤버의 사용은 참조로 인정하지 않는다

이와 같은 문제를 해결하기 위해서는 강제로 멤버를 참조해야만 한다. 여러분은 정적 멤버 함수를 이용해서 한방에 해결해 보고 싶겠지만, 아쉽게도 링커는 정적 멤버 함수의 사용을 참조로 인정하지 않는다. 즉, AutoPusher 클래스에 다음과 같은 정적 멤버 함수와 변수를 사용한다 하더라도 문제를 피할 수 없다는 얘기다.

 static void StaticTouch() 
 {
    _staticDummyValue = 0;
 }
 static int _staticDummyValue;
 
 // AutoPusher::StaticTouch() 를 호출해준다.

따라서 정적 멤버가 아닌 일반 멤버 함수를 이용해서 문제를 해결해야 한다. 다음은 이 문제를 해결한 코드다.

 void Touch() { _dummyValue = 0; }
 int _dummyValue;
 
 // ap1.Touch(); ap2.Touch(); ap3.Touch(); 를 호출해 준다.

Touch() 멤버 함수에서 멤버 변수를 사용하지 않는다면, 아무리 Touch() 멤버 함수를 호출해도 링커는 그 객체가 참조됐다고 인정하지 않는다. 즉, Touch() 멤버 함수 내부에서는 읽든지 쓰든지, 멤버 변수를 꼭 사용해야 한다.

참조:함수 레벨 링킹

프로젝트에 함수 레벨 링킹이 가능하도록 설정하면 (/Gy), 링커는 함수를 패키징하여 COMDAT(패키징된 함수라고도 부른다)이라는 것으로 만든다. 링커는 이것을 DLL이나 EXE 파일에서 각각의 함수들을 순서대로 배열하거나 제거하기 위해 사용한다. 링커 옵션을 사용해서 COMDAT을 제거하거나(/OPT) 특정 순서대로 배열할 수 있다 (/ORDER). 다음은 함수 레벨 링킹과 관련 옵션이다.

/Gy : 함수 레벨 링킹을 활성화 한다.
/OPT:REF : 참조되지 않은 데이터를 제거한다.
/OPT:NOREF : 참조되지 않은 데이터를 유지한다.
/OPT:ICF : 중복되는 COMDAT을 제거한다.
/OPT:NOICF : 중복되는 COMDAT을 제거하지 않는다.
/ORDER:[파일이름] : COMDAT을 파일에 지정된 순서대로 이미지에 기록한다.

강 기완

네오위즈 B본부 프로그래머.
Close Menu