[C++11] nullptr

C++11에서 새로이 제공하는 nullptr에 대해 알아본다.

식상한 뻔히 다 아는 이야기는 생략하고 싶지만, 이 글로부터 처음 nullptr을 배우는 분들을 위해 소개한다.

nullptr 키워드는 c++11에서 새로 추가된 전역객체 이다.

nullptr이 새롭게 추가된 이유는 기존의 NULL이 c++에서 문제가 생겼기 때문이다.

우선 NULL이 어떻게 정의되어 있는지 한번 살펴보자.

gcc 4.9 stddef.h 의 NULL
/* A null pointer constant.  */

#if defined (_STDDEF_H) || defined (__need_NULL)
#undef NULL        /* in case <stdio.h> has defined it. */
#ifdef __GNUG__
#define NULL __null
#else   /* G++ */
#ifndef __cplusplus
#define NULL ((void *)0)
#else   /* C++ */
#define NULL 0
#endif  /* C++ */
#endif  /* G++ */
#endif    /* NULL not defined and <stddef.h> or need NULL.  */
#undef    __need_NULL
vs2013 stddef.h 의 NULL
/* Define NULL pointer value */
#ifndef NULL
#ifdef __cplusplus
#define NULL    0
#else  /* __cplusplus */
#define NULL    ((void *)0)
#endif  /* __cplusplus */
#endif  /* NULL */

두 컴파일러가 소스는 다르지만 결론은 같다.

C 에서는 NULL은 (void*)0 이고

C++ 에서는 0 이다.

1. 우선 왜 두 언어가 NULL이 다를까?

C언어에서 0 , '\0' , NULL 은 사실 모두 0이다. L-value의 모든 비트를 0으로 세팅하는 용도로 사용된다.

같은 결과이지만, 그 키워드에 의미가 다르다. 0 은 말 그대로 정수 0을 뜻하며

'\0' 은 문자열의 마지막(널 문자)를 나타낸다.

NULL 은 포인터가 아무것도 가르키고 있지 않다는 의미이다.

C언어에서는 void* 가 다른 포인터로 암묵적인 형변환이 가능했다.

하지만 C++에서는 좀더 엄격한 type-checking을 위해 이는 오류가 난다.

아래의 소스는 C언어 컴파일러인 VC(c language) , gcc 에서는 정상적으로 동작한다.

#include<malloc.h>
int main(){  
    char* p = malloc(100);
    return 0;
}

하지만 VC(c plusplus) , g++ 에서는 아래와 같은 오류를 출력한다.

vs2013의 오류
1>------ 빌드 시작: 프로젝트: MyTest0001, 구성: Debug Win32 ------  
1>  main.cpp  
1>c:\users\kimbom\documents\visual studio 2013\projects\mytest0001\mytest0001\main.cpp(4): error C2440:  
'초기화 중' : 'void *'에서 'char *'(으)로 변환할 수 없습니다.

1>          'void*'에서 'void'가 아닌 포인터로 변환하려면 명시적 캐스트가 필요합니다.  
========== 빌드: 성공 0, 실패 1, 최신 0, 생략 0 ==========
g++의 오류
main.c: In function ‘int main()’:  
main.c:4:22: error: invalid conversion from ‘void*’ to ‘char*’ [-fpermissive]  
  char* p = malloc(100);
                      ^

따라서 C++에서는 울며 겨자먹기로 NULL을 0으로 처리한다.

그 이유는 C/C++ 에서는 포인터를 0으로 초기화 하는것을 허락하기 때문이다. (다른 정수는 안됨)

2. 그렇다면 무엇이 문제가 될까?

너무나도 뻔하다. 오버로딩에서 문제가 생긴다. 긴 예시는 필요없다.

#include<iostream>
#include<cstdlib>
using namespace std;  
void foo(int param){  
        cout << "int param" << endl;
}
void foo(int* param){  
        cout << "int* param" << endl;
}
int main(){  
        foo(NULL);
        return EXIT_SUCCESS;
}
vs2013의 출력
int param  
g++ 4.9의 에러
main.cpp: In function ‘int main()’:  
main.cpp:11:10: error: call of overloaded ‘foo(NULL)’ is ambiguous  
  foo(NULL);
          ^
main.cpp:11:10: note: candidates are:  
main.cpp:4:6: note: void foo(int)  
 void foo(int param){
      ^
main.cpp:7:6: note: void foo(int*)  
 void foo(int* param){
      ^

g++에서는 int 타입과 pointer 타입이 오버로딩 되있는데 NULL을 사용하니 모호하다고 그냥 에러를 뿜어 버린다.(이게 더 좋은거임)

어쨋던 우리의 의도는 NULL이 포인터가 인수인 함수로 가길 원했는데 당연히...int가 인수인 함수로 간다.

보기 드문 에러라고 생각이 든다면 당연한 것이다.(그러니 c++11 이나 되서 나왔지)

3. nullptr의 구현

nullptr의 초기 공개 구현안은 다음과 같이 되어있다.(실제론 더 추가되어있음)

const                             // this is a const object...  
class {  
public:  
  template<class T>               // convertible to any type
    operator T*() const           // of null non-member
    { return 0; }                 // pointer...

 template<class C, class T>       // or any type of null
    operator T C::*() const       // member pointer...
    { return 0; }

private:  
  void operator&() const;         // whose address can't be
                                  // taken (see Item 27)...
}NULLPTR={};                      // and whose name is NULLPTR
typedef decltype(NULLPTR) nullptr_t;  

위의 소스를 분석해보면, 먼저 const는 그냥 해당 전역객체를 생성하는데 한정자를 붙인 것이다.(c++에는 const, static 등은 class에 접두하는 한정자가 아니다.)

전역객체는 마지막 줄에 NULLPTR 이다.

public의 처음 함수는 template을 이용한 모든 포인터 자료형에 대한 형변환 연산을 제공한다.

두번째 2개의 템플릿 인수를 받는 함수가 이해가 잘 가지 않는데,

Official proposal에서 나온 문서에서는 마지막에 아래와 같이 설명하고 있다.

This is a good initial draft, but it can be refined in several ways.  
First, we don't really need more than one NullClass object, so there's no reason to give the class a name;  
we can just use an anonymous class and make NULL of that type. 

Second, as long as we're making it possible to convert NULL to any type of pointer,  
we should handle pointers to members, too. That calls for a second member template, 

one to convert 0 to type T C::* ("pointer to member of type T in class C") for all classes C and all types T.  
(If that makes no sense to you, or if you've never heard of — much less used — pointers to members, relax. 
Pointers to members are uncommon beasts, rarely seen in the wild, and you'll probably never have to deal with them.  
The terminally curious may wish to consult Item 30, which discusses pointers to members in a bit more detail.) 

Finally, we should prevent clients from taking the address of NULL,  
because NULL isn't supposed to act like a pointer, it's supposed to act like a pointer value,  
and pointer values (e.g., 0x453AB002) don't have addresses.  

중간에 두번째 형변환연산에 대해 설명하고 있는데 이를 해석하면 아래와 같다.

우린 멤버에 대한 포인터 역시 처리해야한다.
이것은 2번째 템플릿을 요구한다.
타입 T에 대해 0을 반환한다. C::* (C 클래스의 포인터멤버 T) 는 모든 클래스 C에 대한 모든 타입 T를 뜻한다.
그러나 이런건 아마 당신이 들어본적 없을 것이다. 이건 보기 드문 경우이다. 아마 당신은 이것을 다룰 필요가 없을 것이다.

이게 무슨 해괴한 소리냐면 어떤 클래스 내부에 함수가 하나 있다고 하자. 이 함수는 반환형이 void이고 인수가 없지만 일반 void f(); 함수와 자료형이 다르다. 그 이유는 앞의 함수는 메소드이고 뒤에 함수는 함수이기 때문이다. 객체 지향언어에서 함수와 메소드의 차이를 안다면 이해하기 쉬울것이다.

바로 어떤 클래스 내부에 있는 함수의 포인터자료형을 초기화 할때 바로 위의 두번째 형변환 오버로딩 함수가 호출된다.

아래의 소스에서 두개의 템플릿이 Z는 C로 Z::proc는 T로 처리된다.

class Z{  
public:  
    void proc();
}
int main(){  
    void(Z::*ptr)()=NULLPTR;
    cout << typeid(&Z::proc).name() << endl;
    return 0;
}

마지막으로 nullptr의 주소를 뽑아낼수 없게 private에 주소연산자 오버로딩을 넣어놓았다.

4. 마치며...

일반적으로 NULL을 사용하는곳에 nullptr로 대체할 수 있고, 대체할경우 더 안전한 코드를 짤 수 있다.(실제로 C++에서는 무조건 nullptr을 사용해야한다 이제는)

이 nullptr이 키워드가 아니라 전역객체임을 알고 있다면, 무엇이 가능하고 무엇이 불가능한지는 금방 보인다.

얕게 알고싶으면 C++에서는 NULL 대신 nullptr을 사용하면 되는것이고

깊게 알고싶다면 위의 글을 자세히 읽어보길 바란다. 필요하다면 참고 문서 항목에서 직접 읽어봐도 된다.

실제 구현안은 nullptrbool과 어느정도 호환이 되고, 비교 연산에 대해 어느정도 호환이 되게 구현 해놨다.

실제 구현안과 비슷한 소스역시 참고 문서항목에 링크를 남겨 두었다.


참고 문서

nullptr 공식 제안서
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2431.pdf

nullptr 공식 제안서 설명
http://smoothdock.ccbb.pitt.edu/blogs/weiyi/documents/c

nullptr 위키북스 설명
https://en.m.wikibooks.org/wiki/MoreC%2B%2BIdioms/nullptr

nullptr 실제 구현안과 가장 비슷한 git source
https://github.com/electronicarts/EASTL/blob/master/test/packages/EABase/include/Common/EABase/nullptr.h