신입 게임 개발자의 프로그래밍 일기

[C++ 입문자에서 벗어나기]Chapter_4: 메모리(Memory)#4 - 2 본문

Language/C++

[C++ 입문자에서 벗어나기]Chapter_4: 메모리(Memory)#4 - 2

KFGD 2018. 2. 1. 14:49

[C++ 입문자에서 벗어나기]Chapter_4: 메모리(Memory)#4 - 2


[new 재정의시 유의사항: 이름 가림]


void* operator new(std::size_t count) throw(bad_alloc);

void* operator new(std::size_t count,
    const std::nothrow_t&) throw();

void* operator new(std::size_t count,
    void* ptr) throw();

<참고: Microsoft Developer Network의 <new> operator 글>


이전 포스팅에서 C++ 표준에서 제공하는 new는 세가지 형태가 있다는 것을 알고 있으실 겁니다.

만약 이 중 하나만을 재정의하고 나머지를 기존 형태 그대로 쓰고 싶은 경우, 

혹은 기존에 제공하는 형태가 아닌 새로운 형태(다른 파라미터로 동작하는)의 new를 추가시키고 싶은 때가 발생한다면

재정의하지 않은 나머지 C++표준에서 제공하는 new들을 전부 원하지 않더라도 전부 전달함수를 사용해서 제공해야만 합니다.

이해되지 않으신다면 아래의 코드를 한번 봐주시기 바랍니다.

코드를 보시면 주석으로 (1), (2), (3), (4) 표시된 총 4개의 new가 있음을 확인 할 수 있습니다.

과연 어느 것들이 올바르게 컴파일되어 실행될 수 있을까요?



어느 것들이 맞은 걸까요?

답을 이야기하자면 4번째 코드 빼고는 전부 컴파일 에러가 발생합니다.

그 이유는 적합한 new 코드를 찾을 수 없기 때문인데요.

첫번째 경우에는 std::size_t 파라미터 하나만을 갖는 new가 없기 때문이고

두번째 경우는 예외를 던지지 않는 new가 없기 때문이며

세번째 경우는 void* 타입이 FastMemory* 타입으로 암시적으로 변경되는 것을 허용하지 않기 때문입니다.

(반대의 경우 FastMemory* 타입이 void*타입으로 암시적으로 변경되는 것은 허용합니다.)

코드를 살펴보면 모두 CDerived 인자로 하는 new를 사용하는 것을 볼 수 있는데 

컴파일러는 ADL 규칙에 따라 인수의 타입으로 사용된 CDerived 클래스를 조사합니다.

(ADL에 대해서는 조금있다가 설명드리겠습니다.)

CBase 클래스에서 오직 static void* operator new(std::size_t count, FastMemory* ptr) throw() 형태의 new만을

제공함으로 CBase에서 파생된 CDerived 클래스 역시 new 하나만을 제공하게 됩니다.


위 이슈를 가리켜 이름 가림(Name hiding)이라고 말합니다.

이름 가림 이슈는 "Effective C++ 항목 33: 상속된 이름을 숨기는 일은 피하자"에서도 다뤄지고 있는데

이것은 C++의 유효범위(Scope)와 이름 조회(lookup)와 관련이 있습니다.

위 예시 코드는 상속 관계에서 operator new를 재정의한는 것정도로만 볼 수 있지만 좀 더 시야를 넓게 가져봅시다.

 


우선 전역 범위(Global)에서 보면 표준 C++에서 정의된 세 가지 종류의 new가 존재합니다.

그런데 CBase 클래스를 정의하고 그곳에 표준 C++에서 정의한 종류가 아닌 

완전히 다른 형태의 FastMemory*를 인자로 가진 new를 정의하였습니다.

이 상황에서 CBase 클래스(범위)안에서 정의한 new로 인해 동일한 이름을 가진 표준 C++에서 정의하는 

new들이 가려지는 이름 가림 이슈가 발생하게 됩니다.


(참고적으로 여기서 말하는 이름은 함수의 프로토타입(Prototype) 전체를 이야기하는 것이 아닌

온전히 함수의 이름(Name)만을 고려합니다. 인수와 반환형은 따지지 않음을 유의해주세요!)


물론, 아예 표준 C++에서 제공하는 new를 사용 할 수 없는 것은 아닙니다.

범위 연산자( :: , Scope Resoultion Operator)를 사용하면 가능합니다.



[Argument-dependent lookup(ADL, KoenigLookup)]

위 예시 코드에 제시된 operator new의 경우는 Argument-dependent lookup(ADL)방식을 따른다고 살짝 언급했는데 이제 그 ADL이 뭔지를 알아보려고 합니다.

이번에는 상속관계를 빼고 operator 연산자 오버로딩도 뺀 좀 더 단순한 코드로 ADL을 살펴보겠습니다.




위 코드에는 전역 범위를 제외하고도 한 개의 이름 공간(namesapce)인 NS가 더 존재합니다.

각각의 전역 범위와 이름 공간들 안에는 완전히 동일한 구조의 int value 와 struct X{} , void f(const X& x)가 있음을 확인할 수 있을겁니다.

그렇다면 최종적으로는 총 2개의 똑같은 int value 와 struct X{} , void f(const X& x)가 있다는 것인데 

위 예시 코드를 컴파일시켜보면 과연 몇개의 코드가 정상적으로 동작할까요?

맞습니다. 이 코드는 정상적인 변수 사용 및 함수 호출들로 구성되어 있습니다.

이제부터 이 코드들을 이름 조회(Name lookup)시점에서 살펴보겠습니다.

먼저 이름 검색는 간단하게 이야기하자면 아래와 같습니다.


이름 검색(Name lookup)란?

-프로그램 속에는 여러 이름(name)들이 사용되는데 이들을 각각 어느 이름 선언(declaration)과 연관(associate)시켜하는 지를 결정하는 절차입니다.

-이름 검색은 범위연산자를 사용하는 한정된 이름 검색(Qualified name lookup)과 

사용하지 않는 비한정된 이름 검색(Unqualified name lookup)으로 나뉘어집니다.


코드에서도 알 수 있겠지만 한정된 이름 검색은 범위 연산자를 사용하여 어느 공간 혹은 범위(Scope)에 정의된 선언를 사용할 지를 지정해주는 이름 검색이고 비한정된 이름 검색은 범위 연산자를 사용하지 않고 현재(호출 코드가 있는) 범위와 가장 근접한 선언을 찾아주는 이름 검색 입니다.

(Variables-1)과 (Function-1), (Function-3)는 비한정된 이름 검색이고 그 이외의 것들을 범위 연산자를 사용한 한정된 이름 검색입니다.

그런데 왜 글쓴이인 저는 변수의 경우는 비한정된 이름 검색이 쓰인 코드를 한가지만 제시하고

함수 호출의 경우는 두 가지를 제시했을까요?

그 이유는 ADL은 비한정된 이름 검색에 속하면서 오직 함수 호출에만 사용되는 이름 검색이기 때문입니다.

함수 코드에서 이미 추측하신 분들도 계시겠지만...


ADL은 함수 호출에 사용된 인자들의 타입을 고려하여 현재 범위와 다른 범위(struct, class, namespace ...)에 있을지라도 적합한 함수를 검색하는 것이 포인트인 이름 검색(Name lookup) 방식입니다.


변수에 적용된 비한정된 이름을 그대로 사용할 경우 (Function-3)의 경우는 에러가 발생할 겁니다.

왜냐하면 전역 범위에 정의된 함수 f는 전역 범위에 정의된 구조체 x를 사용하기 때문입니다.


마지막으로 전달 함수와 using을 사용하는 "Effective C++ 항목 52: 위치지정 new를 작성한다면 위치지정 delete도 같이 준비하자"에서 제시한 operator new & delete를 오버로딩하면서 표준 operator new & delete를 가리지 않는 방식을 적용시킨 

코드 샘플을 보여드리는 것으로 이번 포스팅을 마치도록 하겠습니다.

*using 선언(using-declaration)을 통해 선언한 이름은 비한정된 이름 검색으로 검색됩니다. 



Comments