nullptr을 사용하면 어떤 이점이 있습니까?


163

이 코드는 개념적으로 세 포인터에 대해 동일한 작업을 수행합니다 (안전한 포인터 초기화).

int* p1 = nullptr;
int* p2 = NULL;
int* p3 = 0;

그래서, 포인터를 할당하는 장점이 무엇인가 nullptr그들에게 값을 할당 이상 NULL또는 0?


39
우선, 오버로드 기능을 복용 int하고 void *선택되지 않습니다 int오버 버전을 void *사용하는 경우 버전 nullptr.
chris

2
f(nullptr)다릅니다 f(NULL). 그러나 위의 코드와 관련하여 (로컬 변수에 할당) 세 포인터는 모두 동일합니다. 유일한 장점은 코드 가독성입니다.
balki

2
이 질문을 @Prasoon으로 만드는 것을 선호합니다. 감사!
sbi December

1
NB NULL은 역사적으로 0을 보장하지 않지만, oc C99와 동일합니다. 바이트와 길이가 반드시 8 비트 일 필요는 없으며 아키텍처에 의존하는 값은 true와 false입니다. 에이 질문은 초점을 맞추고 nullptrbutthat 0 사이의 차이점NULL
awiebe

답변:


180

이 코드에는 이점이없는 것 같습니다. 그러나 다음과 같은 오버로드 된 함수를 고려하십시오.

void f(char const *ptr);
void f(int v);

f(NULL);  //which function will be called?

어떤 함수가 호출됩니까? 물론 여기서 의도는을 호출하는 f(char const *)것이지만 실제로 f(int)는 호출 될 것입니다! 그것은 큰 문제입니다 1 , 그렇지 않습니까?

따라서 이러한 문제에 대한 해결책은 다음을 사용하는 것입니다 nullptr.

f(nullptr); //first function is called

물론 이것이 유일한 이점은 아닙니다 nullptr. 여기 또 다른 것이 있습니다 :

template<typename T, T *ptr>
struct something{};                     //primary template

template<>
struct something<nullptr_t, nullptr>{};  //partial specialization for nullptr

템플릿에서의 유형은 nullptr로 추론 nullptr_t되므로 다음 과 같이 작성할 수 있습니다.

template<typename T>
void f(T *ptr);   //function to handle non-nullptr argument

void f(nullptr_t); //an overload to handle nullptr argument!!!

1.에서 C ++ NULL로 정의됩니다 #define NULL 0그것은 기본적 그래서, int왜, f(int)라고합니다.


1
Mehrdad가 말했듯이 이러한 종류의 과부하는 매우 드 rare니다. 다른 관련 장점이 nullptr있습니까? (아니요, 난 요구하지 않습니다)
마크 가르시아

2
@MarkGarcia, 이것이 도움이 될 수 있습니다 : stackoverflow.com/questions/13665349/…
chris

9
각주가 거꾸로 보입니다. NULL표준에는 필수 유형이 있어야하므로 일반적으로 0또는로 정의됩니다 0L. 또한 nullptr_t오버로드가 마음 에 들지 않습니다 . 과 같은 다른 유형의 null 포인터가 아닌 로만 호출을 잡기 때문 입니다. 그러나 나는 그것이 "없음"을 의미하는 자신 만의 단일 값 자리 표시 자 유형을 정의하는 것을 저장하는 것만으로도 용도가 있다고 믿을 수 있습니다. nullptr(void*)0
Steve Jessop

1
nullptr널 포인터 상수는 그렇지 않은 반면, 잘 정의 된 숫자 값 을 갖는 또 다른 이점이있을 수 있습니다 . 널 포인터 상수는 해당 유형의 널 포인터로 변환됩니다 (무엇이든). 동일한 유형의 두 개의 널 포인터가 동일하게 비교되어야하며 부울 변환은 널 포인터를로 바꿉니다 false. 다른 것은 필요하지 않습니다. 따라서, 컴파일러 (어리석지 만 가능할 수 있음)는 예를 들어 0xabcdef1234널 포인터에 다른 숫자 를 사용할 수 있습니다. 반면 nullptr에 숫자 0으로 변환해야합니다.
데이먼

1
@DeadMG : 내 대답에 무엇이 잘못 되었습니까? 그 f(nullptr)의도 된 함수를 호출하지 않습니다? 동기가 두 가지 이상있었습니다. 프로그래머들에 의해 앞으로 다른 많은 유용한 것들이 발견 될 수 있습니다. 그래서 당신이 말할 수없는 단 하나의 진정한 사용nullptr.
Nawaz

87

C ++ 11 컨덕터는 nullptr, 그것은라고도 Null포인터 상수와는 형태 안정성을 향상 하고 모호한 상황 해결 기존의 구현에 의존 NULL 포인터 상수를 달리 NULL. 의 장점을 이해할 수 있습니다 nullptr. 먼저 무엇이 무엇 NULL이고 무엇이 관련되어 있는지 이해해야 합니다.


NULL정확히 무엇입니까 ?

Pre C ++ 11 NULL은 값이없는 포인터 나 유효한 것을 가리 키지 않는 포인터를 나타내는 데 사용되었습니다. 대중적인 개념과 달리 NULLC ++의 키워드는 아닙니다 . 표준 라이브러리 헤더에 정의 된 식별자입니다. 즉, NULL표준 라이브러리 헤더를 포함 하지 않으면 사용할 수 없습니다 . 샘플 프로그램을 고려하십시오 .

int main()
{ 
    int *ptr = NULL;
    return 0;
}

산출:

prog.cpp: In function 'int main()':
prog.cpp:3:16: error: 'NULL' was not declared in this scope

C ++ 표준은 NULL을 특정 표준 라이브러리 헤더 파일에 정의 된 구현 정의 매크로로 정의합니다. NULL의 원점은 C에서 왔으며 C ++는 C에서 상속했습니다. C 표준은 NULL을 0또는 로 정의했습니다 (void *)0. 그러나 C ++에는 미묘한 차이가 있습니다.

C ++은이 사양을 그대로 받아 들일 수 없습니다. C와 달리 C ++는 강력한 유형의 언어입니다 (C는 void*어떤 유형 으로도 명시 적 캐스트를 요구하지 않지만 C ++은 명시 적 캐스트를 요구합니다). 이것은 많은 C ++ 표현식에서 C 표준으로 지정된 NULL 정의를 쓸모 없게 만듭니다. 예를 들면 다음과 같습니다.

std::string * str = NULL;         //Case 1
void (A::*ptrFunc) () = &A::doSomething;
if (ptrFunc == NULL) {}           //Case 2

NULL이로 정의 된 경우 (void *)0위의 표현식 중 어느 것도 작동하지 않습니다.

  • 사례 1 : 자동 캐스트에서 필요하기 때문에 컴파일되지 않습니다 void *std::string.
  • 사례 2 :void * 멤버 함수로의 캐스트에서 캐스트 가 필요 하기 때문에 컴파일되지 않습니다 .

따라서 C와 달리 C ++ Standard는 NULL을 숫자 리터럴 0또는 로 정의해야합니다 0L.


그렇다면 NULL이미 널 포인터 상수가 필요 합니까?

C ++ 표준위원회는 C ++에서 작동하는 NULL 정의를 만들었지 만이 정의에는 고유 한 문제가 있습니다. NULL은 거의 모든 시나리오에 충분하지만 전부는 아닙니다. 특정 희귀 시나리오에 대해 놀랍고 잘못된 결과를 제공했습니다. 예를 들면 다음과 같습니다.

#include<iostream>
void doSomething(int)
{
    std::cout<<"In Int version";
}
void doSomething(char *)
{
   std::cout<<"In char* version";
}

int main()
{
    doSomething(NULL);
    return 0;
}

산출:

In Int version

분명히, char*인수로 사용되는 버전을 호출하는 것으로 보이지만 출력에 int버전을 가져 오는 함수가 표시됨에 따라 호출됩니다. NULL은 숫자 리터럴이기 때문입니다.

또한 NULL이 0 또는 0L인지 여부는 구현 정의이므로 함수 과부하 해결에 많은 혼란이있을 수 있습니다.

샘플 프로그램 :

#include <cstddef>

void doSomething(int);
void doSomething(char *);

int main()
{
  doSomething(static_cast <char *>(0));    // Case 1
  doSomething(0);                          // Case 2
  doSomething(NULL)                        // Case 3
}

위의 스 니펫 분석 :

  • 사례 1 :doSomething(char *) 예상대로 전화 .
  • 사례 2 : IS도 널 포인터 이기 때문에 호출 doSomething(int)하지만 char*버전이 필요할 수 있습니다 0.
  • 사례 3 :NULL 로 정의 된 경우 의도 했을 때 0호출 하여 런타임시 논리 오류가 발생할 수 있습니다. 로 정의 된 경우 호출이 모호하여 컴파일 오류가 발생합니다.doSomething(int)doSomething(char *)NULL0L

따라서 구현에 따라 동일한 코드로 다양한 결과를 얻을 수 있으며 이는 바람직하지 않습니다. 당연히 C ++ 표준위원회는이 문제를 해결하기를 원했고 이것이 nullptr의 주요 동기입니다.


그렇다면 무엇 nullptr이고 어떻게 문제를 피할 수 NULL있습니까?

C ++ 11 nullptr은 널 포인터 상수로 사용할 새 키워드 를 도입했습니다 . NULL과 달리 동작은 구현 정의되지 않습니다. 매크로는 아니지만 자체 유형이 있습니다. nullptr의 타입이 std::nullptr_t있습니다. C ++ 11은 NULL의 단점을 피하기 위해 nullptr의 속성을 적절하게 정의합니다. 속성을 요약하려면 다음을 수행하십시오.

속성 1 : 자체 유형 std::nullptr_t이 있고
속성 2 : 암시 적으로 변환 가능하고 모든 포인터 유형 또는 포인터 대 멤버 유형과 비교 가능하지만
속성 3 :을 제외 하고는 암시 적으로 변환하거나 정수 유형과 비교할 수 없습니다 bool.

다음 예제를 고려하십시오.

#include<iostream>
void doSomething(int)
{
    std::cout<<"In Int version";
}
void doSomething(char *)
{
   std::cout<<"In char* version";
}

int main()
{
    char *pc = nullptr;      // Case 1
    int i = nullptr;         // Case 2
    bool flag = nullptr;     // Case 3

    doSomething(nullptr);    // Case 4
    return 0;
}

위 프로그램에서

  • 사례 1 : 확인-속성 2
  • 사례 2 : 양호하지 않음-속성 3
  • 사례 3 : 확인-속성 3
  • 사례 4 : 혼란 없음-통화 char *버전, 속성 2 및 3

따라서 nullptr을 도입하면 좋은 오래된 NULL의 모든 문제를 피할 수 있습니다.

어떻게 그리고 어디에서 사용해야 nullptr합니까?

C ++ 11의 경험 법칙은 nullptr과거에 NULL을 사용 했을 때마다 간단히 사용하기 시작 합니다.


표준 참조 :

C ++ 11 표준 : C.3.2.4 매크로 NULL
C ++ 11 표준 : 18.2 유형
C ++ 11 표준 : 4.10 포인터 변환
C99 표준 : 6.3.2.3 포인터


나는 알고 있기 때문에 마지막 조언을 이미 연습 nullptr하고 있지만 실제로 내 코드와 어떤 차이가 있는지는 알지 못했습니다. 큰 답변과 특히 노력에 감사드립니다. 주제에 대해 많은 정보를 얻었습니다.
Mark Garcia

"특정 표준 라이브러리 헤더 파일에서." 왜 처음부터 "cstddef"라고 쓰지 않겠습니까?
mxmlnkn

nullptr을 bool 유형으로 변환 할 수있는 이유는 무엇입니까? 좀 더 자세히 설명해 주시겠습니까?
Robert Wang

... 값이없는 포인터를 나타내는 데 사용되었습니다. 변수 에는 항상 값이 있습니다. 노이즈 일 수도 0xccccc....있지만 값이없는 변수는 본질적인 모순입니다.
3Dave

"사례 3 : 확인-속성 3"(줄 bool flag = nullptr;). 아니, 괜찮아, g ++ 6 컴파일 시간에 다음과 같은 오류가 발생합니다error: converting to ‘bool’ from ‘std::nullptr_t’ requires direct-initialization [-fpermissive]
Georg

23

여기서 진정한 동기는 완벽한 전달 입니다.

치다:

void f(int* p);
template<typename T> void forward(T&& t) {
    f(std::forward<T>(t));
}
int main() {
    forward(0); // FAIL
}

간단히 말해, 0은 특별한 이지만 값은 시스템 전용 유형을 통해 전파 될 수 없습니다. 전달 기능은 필수이며 0은 처리 할 수 ​​없습니다. 따라서, 타입 이 특별한 것이고, 타입이 실제로 전파 될 수 nullptr있는 곳 을 소개하는 것이 절대적으로 필요했습니다 . 실제로, MSVC 팀은 rvalue 참조를 구현 한 후이 함정을 스스로 발견 한 후 일정보다 앞서 소개 해야했습니다.nullptr

nullptr인생이 더 쉬워 질 수 있는 몇 가지 다른 경우가 있지만, 캐스트가 이러한 문제를 해결할 수 있기 때문에 핵심 사례는 아닙니다. 치다

void f(int);
void f(int*);
int main() { f(0); f(nullptr); }

두 개의 별도 과부하를 호출합니다. 또한, 고려

void f(int*);
void f(long*);
int main() { f(0); }

모호합니다. 그러나 nullptr을 사용하면

void f(std::nullptr_t)
int main() { f(nullptr); }

7
이상한. 답변의 절반은 다른 두 답변과 동일합니다. 귀하에 따르면 "정확하지 않은" 답변입니다.
Nawaz

전송 문제는 캐스트로 해결할 수도 있습니다. forward((int*)0)공장. 뭔가 빠졌습니까?
jcsahnwaldt Reinstate Monica

5

nullptr의 기초

std::nullptr_t널 포인터 리터럴 인 nullptr의 유형입니다. 유형의 prvalue / rvalue입니다 std::nullptr_t. nullptr에서 모든 포인터 유형의 널 포인터 값으로의 암시 적 변환이 있습니다.

리터럴 0은 포인터가 아닌 정수입니다. C ++이 포인터 만 사용할 수있는 컨텍스트에서 0을보고 있으면 0을 널 포인터로 간결하게 해석하지만 대체 위치입니다. C ++의 기본 정책은 0이 포인터가 아니라 int라는 것입니다.

장점 1-포인터 및 정수 유형에 과부하가 걸리는 경우 모호함 제거

C ++ 98에서 이것의 주된 의미는 포인터와 정수형에 과부하가 걸리면 놀라움을 줄 수 있다는 것입니다. 이러한 과부하에 0 또는 NULL을 전달하면 결코 포인터 과부하를 호출하지 않습니다.

   void fun(int); // two overloads of fun
    void fun(void*);
    fun(0); // calls f(int), not fun(void*)
    fun(NULL); // might not compile, but typically calls fun(int). Never calls fun(void*)

이 호출에 대한 흥미로운 점은 소스 코드의 명백한 의미 ( "Null 포인터로 재미를 부르고 있습니다")와 실제 의미 ( "Null이 아닌 어떤 종류의 정수로 재미를 부르고 있습니다"라는 모순입니다. 바늘").

nullptr의 장점은 정수 유형이 없다는 것입니다. nullptr을 적분으로 볼 수 없기 때문에 nullptr로 오버로드 된 함수 fun을 호출하면 void * 오버로드 (예 : 포인터 오버로드)가 호출됩니다.

fun(nullptr); // calls fun(void*) overload 

따라서 0 또는 NULL 대신 nullptr을 사용하면 과부하 해결 놀라움을 피할 수 있습니다.

의 또 다른 장점 nullptr을 통해 NULL(0)반환 형식에 대한 자동을 사용하는 경우

예를 들어, 코드베이스에서이 문제가 발생한다고 가정 해보십시오.

auto result = findRecord( /* arguments */ );
if (result == 0) {
....
}

findRecord가 무엇을 리턴하는지 알 수 없거나 쉽게 찾을 수없는 경우 결과가 포인터 유형인지 정수 유형인지 명확하지 않을 수 있습니다. 결국 0 (결과가 테스트 된 결과)은 어느 쪽이든 갈 수 있습니다. 반면에 다음을 보면

auto result = findRecord( /* arguments */ );
if (result == nullptr) {
...
}

모호성이 없습니다. 결과는 포인터 유형이어야합니다.

장점 3

#include<iostream>
#include <memory>
#include <thread>
#include <mutex>
using namespace std;
int f1(std::shared_ptr<int> spw) // call these only when
{
  //do something
  return 0;
}
double f2(std::unique_ptr<int> upw) // the appropriate
{
  //do something
  return 0.0;
}
bool f3(int* pw) // mutex is locked
{

return 0;
}

std::mutex f1m, f2m, f3m; // mutexes for f1, f2, and f3
using MuxtexGuard = std::lock_guard<std::mutex>;

void lockAndCallF1()
{
        MuxtexGuard g(f1m); // lock mutex for f1
        auto result = f1(static_cast<int>(0)); // pass 0 as null ptr to f1
        cout<< result<<endl;
}

void lockAndCallF2()
{
        MuxtexGuard g(f2m); // lock mutex for f2
        auto result = f2(static_cast<int>(NULL)); // pass NULL as null ptr to f2
        cout<< result<<endl;
}
void lockAndCallF3()
{
        MuxtexGuard g(f3m); // lock mutex for f2
        auto result = f3(nullptr);// pass nullptr as null ptr to f3 
        cout<< result<<endl;
} // unlock mutex
int main()
{
        lockAndCallF1();
        lockAndCallF2();
        lockAndCallF3();
        return 0;
}

위의 프로그램은 성공적으로 컴파일되고 실행되었지만 lockAndCallF1, lockAndCallF2 & lockAndCallF3에는 중복 코드가 있습니다. 이러한 모든 템플릿을 작성할 수 있다면 이와 같은 코드를 작성하는 것이 유감 lockAndCallF1, lockAndCallF2 & lockAndCallF3입니다. 따라서 템플릿으로 일반화 할 수 있습니다. 중복 코드에 대한 lockAndCall다중 정의 대신 템플릿 기능을 작성했습니다 lockAndCallF1, lockAndCallF2 & lockAndCallF3.

코드는 다음과 같이 리팩터링됩니다.

#include<iostream>
#include <memory>
#include <thread>
#include <mutex>
using namespace std;
int f1(std::shared_ptr<int> spw) // call these only when
{
  //do something
  return 0;
}
double f2(std::unique_ptr<int> upw) // the appropriate
{
  //do something
  return 0.0;
}
bool f3(int* pw) // mutex is locked
{

return 0;
}

std::mutex f1m, f2m, f3m; // mutexes for f1, f2, and f3
using MuxtexGuard = std::lock_guard<std::mutex>;

template<typename FuncType, typename MuxType, typename PtrType>
auto lockAndCall(FuncType func, MuxType& mutex, PtrType ptr) -> decltype(func(ptr))
//decltype(auto) lockAndCall(FuncType func, MuxType& mutex, PtrType ptr)
{
        MuxtexGuard g(mutex);
        return func(ptr);
}
int main()
{
        auto result1 = lockAndCall(f1, f1m, 0); //compilation failed 
        //do something
        auto result2 = lockAndCall(f2, f2m, NULL); //compilation failed
        //do something
        auto result3 = lockAndCall(f3, f3m, nullptr);
        //do something
        return 0;
}

컴파일이 실패 이유를 상세 분석 lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)을 위해하지lockAndCall(f3, f3m, nullptr)

왜 컴파일이 lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)실패 했습니까?

문제는 lockAndCall에 0을 전달하면 템플릿 유형 공제가 유형을 파악하기 시작합니다. 0의 유형은 int이므로 lockAndCall에 대한이 호출의 인스턴스화 내에서 매개 변수 ptr의 유형입니다. 불행히도, 이것은 lockAndCall 내부의 func 호출에서 int가 전달되고 있으며 예상 되는 std::shared_ptr<int>매개 변수 와 호환되지 않음을 의미합니다 f1. 호출에 전달 된 0 lockAndCall은 널 포인터를 나타 내기위한 것이지만 실제로 전달 된 것은 int입니다. 이 int를 f1에 전달하려고 std::shared_ptr<int>하면 유형 오류가 발생합니다. lockAndCall템플릿 내에서 int가을 (를) 요구하는 함수에 전달되어 0으로 호출이 실패합니다 std::shared_ptr<int>.

관련된 통화에 대한 분석 NULL은 본질적으로 동일합니다. NULL에 전달 되면 lockAndCall매개 변수 ptr에 대해 정수 유형이 추론되고 ptrint 또는 int와 유사한 유형이에 전달 될 때 유형 오류가 발생 f2하여를 얻을 것으로 예상합니다 std::unique_ptr<int>.

반대로, 관련된 전화 nullptr에는 문제가 없습니다. 시 nullptr에 전달 lockAndCall을 위해이 유형 ptr추론되어있을 수 있습니다 std::nullptr_t. ptr로 전달 되면 f3을 ( std::nullptr_tint*) std::nullptr_t암시 적으로 모든 포인터 유형으로 변환하기 때문에 에서 (으) 로의 암시 적 변환이 있습니다.

널 포인터를 참조 할 때마다 0 또는가 아닌 nullptr을 사용하는 것이 좋습니다 NULL.


4

nullptr예제를 보여준 방식을 사용하면 직접적인 이점이 없습니다 .
그러나 동일한 이름을 가진 2 개의 기능이있는 상황을 고려하십시오. 1 테이크 int와 다른int*

void foo(int);
void foo(int*);

foo(int*)NULL을 전달 하여 호출하려는 경우 방법은 다음과 같습니다.

foo((int*)0); // note: foo(NULL) means foo(0)

nullptr쉽고 직관적으로 만듭니다 .

foo(nullptr);

Bjarne 웹 페이지의 추가 링크 .
관련이 없지만 C ++ 11 측면 참고 사항 :

auto p = 0; // makes auto as int
auto p = nullptr; // makes auto as decltype(nullptr)

3
참고로 decltype(nullptr)입니다 std::nullptr_t.
chris

2
@MarkGarcia, 내가 아는 한 본격적인 유형입니다.
chris

5
@ MarkGarcia, 흥미로운 질문입니다. cppreference는 다음과 같습니다 typedef decltype(nullptr) nullptr_t;. 나는 표준을 볼 수 있다고 생각합니다. Ah, 그것을 발견 : 참고 : std :: nullptr_t는 포인터 유형이나 멤버 유형에 대한 포인터가 아닌 구별 유형입니다. 오히려이 유형의 prvalue는 널 포인터 상수이며 널 포인터 값 또는 널 멤버 포인터 값으로 변환 될 수 있습니다.
chris

2
@DeadMG : 동기가 두 가지 이상있었습니다. 프로그래머들에 의해 앞으로 다른 많은 유용한 것들이 발견 될 수 있습니다. 그래서 당신이 말할 수없는 단 하나의 진정한 사용nullptr.
Nawaz

2
@DeadMG : 그러나 당신은이 답변이 당신의 답변에서 말한 "진정한 동기" 에 대해 말하지 않기 때문에 "정확 하지 않다 " 고 말했다. 이 답변뿐만 아니라 귀하로부터 공감대를 받았습니다.
Nawaz

4

다른 사람들이 이미 말했듯이, 주요 장점은 과부하입니다. 명시 적 intvs. 포인터 오버로드는 드물지만 std::fill(C ++ 03에서 두 번 이상 물린) 표준 라이브러리 함수를 고려하십시오 .

MyClass *arr[4];
std::fill_n(arr, 4, NULL);

컴파일하지 않습니다 : Cannot convert int to MyClass*.


2

오버로드 문제보다 IMO가 더 중요합니다. 깊이 중첩 된 템플릿 구성에서는 유형을 잃지 않기가 어렵고 명시적인 서명을 제공하는 것은 매우 어려운 작업입니다. 따라서 사용하는 모든 것의 의도 된 목적에보다 정확하게 초점을 맞출수록 명시 적 서명의 필요성이 줄어들고 무언가 잘못 될 경우 컴파일러가보다 통찰력있는 오류 메시지를 생성 할 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.