Post List

2015년 4월 22일 수요일

item 08: 0 , NULL 대신 nullptr 을 사용합시다. (MVA Version)

작가
Meyers, Scott
출판
O'ReillyMedia
발매
2014.12.12
평점








Effective Modern C++ - Scott Meyers
Item 8: Prefer nullptr to 0 and NULL.

Item 8. 0 이나 NULL 대신 nullptr을 사용하자.

이번 장에서는 C++11에 새로 생긴 키워드 nullptr에 대해서 알아보겠습니다.

다룰 내용에 대해서는

1. C++98에서는 왜 NULL을 ???

2. 어떤 문제가 있었는지

3 Overloading Resolution Rule

4. C++11 nullptr에 대한 소개

5. nullptr예제

의 순서대로 말씀드리겠습니다.

* C++98에서는 왜 NULL 을 ???

C++98에서부터 Null Pointer를 표현하기 위해서 NULL을 사용하였습니다.

#define NULL 0

하지만 NULL은 0의 다른 표현일 뿐이지 Null Pointer 가 아닙니다.
왜 이렇게 썼을까요 ?
이유는 간단합니다.
Null Pointer를 표현 할 수 있는 수단이 없었습니다.
그래서 어쩔수 없이 NULL로 사용을 하면서,
같은 함수 이름으로 포인터와 정수형 타입의 오버로딩을 자제하라는 권고안까지 있었습니다.
왜 자제 하라고 했을까요 ?
당연히 문제가 생길수 있으니깐 자제 하라고 했습니다.
그럼 문제가 많은데 왜 그렇게 썼을까요 ?
다른 방법이 없었으니깐요.
그러니깐 사용은 하되, 오버로딩은 자제를...
왜 자제하라고 할까요 ???
(의문의 악순환이...)

* 어떤 문제가 ?

이미 많이 알려진 문제라서 그냥 PASS 하려 했지만, 간단하게 살펴보겠습니다.

#define NULL_PTR 0L

void func(int) {};
void func(bool) {};
void func(void*) {};

func(0);           // calls func(int)
func(NULL);        // calls func(int)
func(NULL_PTR);    // calls func(int)
func((void*)NULL); // calls func(void*)

Visual Studio 2015 CTP에서 사용한 함수에 마우스를 살포시 올려주면,
같은 오버로딩된 함수를 사용하는 것들의 배경색이 살포시 바꿔줍니다.
(void*)로 강제로 Casting 한경우를 제외하고는 모두 func(int)가 호출되었습니다.
심지어 int가 아닌 long 타입에 대해서도 func(int)가 호출되었습니다.
왜 그런지에 대해서 이해하려면
Overloading Resolution Rule 을 아셔야 합니다.

* Overloading Resolution Rule

Overloading Resolution Rule을 자세히 다루는 item이 뒤에 준비되어 있으므로,
현재 item에서의 이해를 돕는 정도로 간단히만 설명 드리겠습니다.

설마 Overloading이 뭔지 모르시는 분이 이것을 보고 계시지는 않겠지만,
그래도 짧게 설명 드리자면,

- Overloading : 같은 이름을 가진 함수를 중복으로 선언하는 것을 말합니다.

- Overloading 조건 : 인자 목록만 다르면 됩니다.
                           여기서 다르다는건 타입 종류 및 순서 입니다.
                           인자의 이름은 조건에 관여하지 않습니다.
                           return 타입은 오버로딩 조건에 관여하지 않습니다.

위까지는 대부분 아시는 내용이라 생각됩니다.
이제부터 조금 머리가 아파지기 시작 할 수도 있습니다.

먼저 각 Code 위에는 아래와 같은 구문이 있다고 가정합니다.

#include <iostream>

using std::cout;
using std::endl;
using std::string;

class Widget {
public:
    int value = 0;
};

1. 기본타입은 const 와의 공존을 거부하지만 (오버로딩 되지 않지만)
   레퍼런스은 const 로 오버로딩 가능하며,
   포인터   도 const 로 오버로딩 가능합니다.

기본 타입에 대해서는 const를 붙이니 바로 컴파일 타임에 오류를 발생시킵니다.

void Func(int a) { OUT("Func(int)"); };
void Func(const int a) { OUT("Func(const int)"); };
// error C2084: func 'void Func(int)' already has a body

레퍼런스에 대해서는 이쁘게 동작합니다.    

void Func(int& a) { OUT("Func(int&)"); };
void Func(const int& a) { OUT("Func(const int&)"); };

int N = 10;

Func(10); // 10 is R-Value : call void Func(const int& a)
Func(N);  // a  is L-Value : call void Func(int& a)

포인터를 추가해도 역시나 이쁘게 동작합니다.

void Func(int& a) { OUT("Func(int&)"); };
void Func(const int& a) { OUT("Func(const int&)"); };

void Func(int* a) { OUT("Func(int*)"); };
void Func(const int* a) { OUT("Func(const int*)"); };

int N = 10;
const int* CPN = &N;
const int CN = 10;

Func(10);  // 10  is R-Value       : call void Func(const int& a)
Func(N);   // a   is L-Value       : call void Func(int& a)
Func(&N);  // &N  is Pointer       : call void Func(int* a)
Func(CPN); // CPN is const Pointer : call void void Func(const int* a)
Func(&CN); // &CN is const Pointer : call void void Func(const int* a)

2. 기본타입은 레퍼런스와 공존 할 수가 없습니다.

기본타입의 함수는 바로 컴파일 할때부터 짜증을 냅니다.

void Func(int a) { OUT("Func(int)"); };
void Func(int& a) { OUT("Func(int&)"); };
// error C2665: 'Func': none of the 2 overloads could convert all the argument types

당연하겠지만 1과 2의 짬뽕인 const int&에 대해서도 공존을 거부합니다.

void Func(int a) { OUT("Func(int&)"); };
void Func(const int& a) { OUT("Func(const int&)"); };
// error C2665: 'Func': none of the 2 overloads could convert all the argument types

참 이기적인 녀석이죠. 지 혼자 먹고 살겠다니깐 말이죠.
일단 여기서는 이까지만 설명드리겠습니다.
자세한 내용은 아래 Link 참조하시면 볼 수 있습니다.

http://en.cppreference.com/w/cpp/language/overload_resolution

* C++11 nullptr
  (Visual Studio 2010부터 사용이 가능합니다.)

C++11 에 드디어 nullptr이 등장하였습니다.
 nullptr을 사용하는게 과거의 NULL을 사용했을 때에 비해 뭐가 더 좋아졌을까요 ?

1. 모든 종류의 Raw pointer 타입으로 암묵적 변환 (implicit casting)이 가능

이건 int 타입도 아니고 포인터 타입도 아닙니다.
std::nullptr_t 타입으로 되어 있으며, 모든 종류의 Raw pointer 타입으로 암묵적 변환 (implicit casting)이 가능합니다.

func(nullptr); // calls func(void*)

nullptr은 정수형으로 취급 될 수가 없으므로  func(void*)이 정상적으로 호출됩니다.

2. 가독성 (Readability) 향상

Code의 Readability도 높여 줄 수 있는데,
특히 auto와 같이 사용했을 때 더더욱 그렇습니다.

auto OBJ = FindObj();

if (OBJ == 0) { ... }

위 Code에서 FindObj()의 return 타입이 정수형(e.g. int)라고 해석을 할 수도 있습니다.
물론 다른 그 어떤 타입일 수도 있겠지만요.

auto OBJ = FindObj();

if (OBJ == NULL) { ... }

이 Code를 한번 보면... 음...
FindObj()의 return 타입이 Pointer 타입 일 수도 있고... 아니면 다른 어떤 Object의 타입일 수도 있고... 물론 int일 수도 있고...
Code의 다른 부분을 좀 더 봐야 정확히 알 수 있겠단 생각이 들겁니다.
이 Code만 보고 타입이 뭐다 라고 말할 수는 없겠네요.

auto OBJ = FindObj();

if (OBJ == nullptr) { ... }

이 Code는 누가 봐도 FindObj()의 return 타입은 Pointer 입니다.
다르게 해석하기가 더 힘듭니다.

하지만 여전히 nullptr 대신 0 이나 NULL을 사용해도 무방한 Code인건 마찬가지입니다.

* nullptr 예제
  template에서 인자의 decltype으로 return 타입추론
 => 누가봐도 NULL이랑 nullptr이 다르겠지.

template 에서 인자의 decltype 으로 return 타입을 추론하는 경우,
그냥 누가봐도... 까지는 아니지만 조금만 생각해보면
"아~ nullptr과 NULL이 다르게 동작하겠구나." 라는 생각이 들 것입니다.

Code를 한번 보고 말씀드리겠습니다.

특정 함수들이 Lock 상태에서만 호출 되어야 하는 경우를 살펴 보겠습니다.

int    FuncSP(std::shared_ptr<Widget> SPW);
double FuncUP(std::unique_ptr<Widget> UPW);
bool   FuncRP(Widget* RPW);

std::mutex MS, MU, MR;

using LOCK = std::lock_guard<std::mutex>; // C++11 : 조금 고급진 typedef

{
LOCK L(MS); // RAII 지원하는 lock_guard Code 고급지게
auto RET = FuncSP(0);
}
{
LOCK L(MU);
auto RET = FuncUP(NULL);
}
{
LOCK L(MR);
auto RET = FuncUP(nullptr);
}

하지만 역시나 nullptr 대신 0 이나 NULL을 사용해도 무방한 Code입니다.
근데, 같은 Code가 3번이나 반복되었습니다.
template을 사용하여 좀 더 고급지게 만들어보도록 하죠.

template<typename FUNC,
         typename MUTEX,
         typename PTR>
auto LockAndFunc(FUNC F, MUTEX& M, PTR P) -> decltype(F(P))
{
LOCK L(M);
return F(P);
}

C++11 에서는 위와 같이 써야하고 C++14에서는 좀 더 간단하게도 표현이 됩니다.

template<typename FUNC,
         typename MUTEX,
         typename PTR>
decltype(auto) LockAndFunc(FUNC F, MUTEX& M, PTR P)
{
LOCK L(M);
return F(P);
}

이제 위의 3개의 함수를 호출해봅시다.

auto RS = LockAndFunc(FuncSP, MS, 0);         // error : can't convert int to std::shared_ptr<Widget>
auto RU = LockAndFunc(FuncUP, MU, NULL);  // error
auto RR = LockAndFunc(FuncRP, MR, nullptr); // OK

0 과 NULL은 언제나 정수형으로 추론되기 때문에 std::shared_ptr<Widget>같은 타입으로는 추론 할 수가 없습니다.
반대로 nullptr은 문제없이 사용할 수 있습니다.

Things to Remember

* 0 과 NULL은 보다는 nullptr을 사용합시다.

* 정수형과 pointer 타입의 overloading은 피합시다.


* MVA용 Slide


댓글 없음:

댓글 쓰기