简体   繁体   English

使用nullptr有什么好处?

[英]What are the advantages of using nullptr?

This piece of code conceptually does the same thing for the three pointers (safe pointer initialization): 这段代码在概念上对三个指针(安全指针初始化)做了同样的事情:

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

And so, what are the advantages of assigning pointers nullptr over assigning them the values NULL or 0 ? 那么,分配指针nullptr不是为它们赋值NULL0什么好处?

In that code, there doesn't seem to be an advantage. 在该代码中,似乎没有优势。 But consider the following overloaded functions: 但请考虑以下重载函数:

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

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

Which function will be called? 将调用哪个函数? Of course, the intention here is to call f(char const *) , but in reality f(int) will be called! 当然,这里的意图是调用f(char const *) ,但实际上会调用f(int) That is a big problem 1 , isn't it? 那是个大问题1 ,不是吗?

So, the solution to such problems is to use nullptr : 所以,解决这些问题的方法是使用nullptr

f(nullptr); //first function is called

Of course, that is not the only advantage of nullptr . 当然,这不是nullptr的唯一优势。 Here is another: 这是另一个:

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

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

Since in template, the type of nullptr is deduced as nullptr_t , so you can write this: 因为在模板中, 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. In C++, NULL is defined as #define NULL 0 , so it is basically int , that is why f(int) is called. 1.在C ++中, NULL定义为#define NULL 0 ,因此它基本上是int ,这就是调用f(int)原因。

C++11 introduces nullptr , it is known as the Null pointer constant and It improves type safety and resolves ambiguous situations unlike the existing implementation dependent null pointer constant NULL . C ++ 11引入了nullptr ,它被称为Null指针常量,它改进了类型安全性解决了模糊情况,这与现有的依赖于实现的空指针常量NULL To be able to understand the advantages of nullptr . 能够理解nullptr的优点。 we first need to understand what is NULL and what are the problems associated with it. 我们首先需要了解什么是NULL以及与之相关的问题。


What is NULL exactly? 什么是NULL

Pre C++11 NULL was used to represent a pointer that has no value or pointer that does not point to anything valid. Pre C ++ 11 NULL用于表示没有值的指针或指向无效的指针。 Contrary to the popular notion NULL is not a keyword in C++ . 与流行的概念相反, NULL不是C ++中的关键字 It is an identifier defined in standard library headers. 它是标准库头中定义的标识符。 In short you cannot use NULL without including some standard library headers. 简而言之,如果不包含一些标准库头,则不能使用NULL Consider the Sample program : 考虑示例程序

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

Output: 输出:

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

The C++ standard defines NULL as an implementation defined macro defined in certain standard library header files. C ++标准将NULL定义为在某些标准库头文件中定义的实现定义宏。 The origin of NULL is from C and C++ inherited it from C. The C standard defined NULL as 0 or (void *)0 . NULL的来源来自C和C ++从C继承.C标准将NULL定义为0(void *)0 But in C++ there is a subtle difference. 但在C ++中有一个微妙的区别。

C++ could not accept this specification as it is. C ++不能接受这个规范。 Unlike C, C++ is a strongly typed language (C does not require explicit cast from void* to any type, while C++ mandates a explicit cast). 与C不同,C ++是一种强类型语言(C不需要从void*到任何类型的显式转换,而C ++强制要求显式转换)。 This makes the definition of NULL specified by C standard useless in many C++ expressions. 这使得C标准指定的NULL定义在许多C ++表达式中无用。 For example: 例如:

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

If NULL was defined as (void *)0 , neither of above expressions would work. 如果将NULL定义为(void *)0 ,则上述表达式都不起作用。

  • Case 1: Will not compile because a automatic cast is needed from void * to std::string . 情况1:无法编译,因为从void *std::string需要自动std::string
  • Case 2: Will not compile because cast from void * to pointer to member function is needed. 情况2:无法编译,因为需要从void *为指向成员函数的指针。

So unlike C, C++ Standard mandated to define NULL as numeric literal 0 or 0L . 因此,与C不同,C ++标准要求将NULL定义为数字文字00L


So what is the need for another null pointer constant when we have NULL already? 那么当我们已经有NULL时,还需要另一个空指针常量吗?

Though the C++ Standards committee came up with a NULL definition which will work for C++, this definition had its own fair share of problems. 虽然C ++标准委员会提出了一个适用于C ++的NULL定义,但这个定义有其自身公平的问题。 NULL worked well enough for almost all scenarios but not all. NULL几乎适用于所有场景,但并非全部。 It gave surprising and erroneous results for certain rare scenarios. 对于某些罕见的情况,它给出了令人惊讶和错误的结果。 For example : 例如

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

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

Output: 输出:

In Int version

Clearly, the intention seems to be to call the version which takes char* as the argument, but as the output shows the function which takes an int version gets called. 显然,目的似乎是调用以char*作为参数的版本,但是输出显示了调用int版本的函数。 This is because NULL is a numeric literal. 这是因为NULL是一个数字文字。

Furthermore, since it is implementation-defined whether NULL is 0 or 0L, there can lot of confusion in function overload resolution. 此外,由于NULL是0或0L是实现定义的,因此在函数重载解析中会有很多混淆。

Sample Program: 示例程序:

#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
}

Analyzing the above snippet: 分析上面的片段:

  • Case 1: calls doSomething(char *) as expected. 情况1:按预期调用doSomething(char *)
  • Case 2: calls doSomething(int) but maybe char* version was be desired because 0 IS also a null pointer. 情况2:调用doSomething(int)但可能需要char*版本,因为0 IS也是空指针。
  • Case 3: If NULL is defined as 0 , calls doSomething(int) when perhaps doSomething(char *) was intended, perhaps resulting in logic error at runtime. 情况3:如果将NULL定义为0 ,则在可能出现doSomething(char *)时调用doSomething(int) ,可能在运行时导致逻辑错误。 If NULL is defined as 0L , the call is ambiguous and results in compilation error. 如果将NULL定义为0L ,则调用不明确并导致编译错误。

So, depending on implementation, the same code can give various outcomes, which is clearly undesired. 因此,根据实现,相同的代码可以提供各种结果,这显然是不希望的。 Naturally, the C++ standards committee wanted to correct this and that is the prime motivation for nullptr. 当然,C ++标准委员会想要纠正这一点,这是nullptr的主要动机。


So what is nullptr and how does it avoid the problems of NULL ? 什么是nullptr以及它如何避免NULL的问题?

C++11 introduces a new keyword nullptr to serve as null pointer constant. C ++ 11引入了一个新的关键字nullptr作为空指针常量。 Unlike NULL, its behavior is not implementation-defined. 与NULL不同,它的行为不是实现定义的。 It is not a macro but it has its own type. 它不是一个宏,但它有自己的类型。 nullptr has the type std::nullptr_t . nullptr的类型为std::nullptr_t C++11 appropriately defines properties for the nullptr to avoid the disadvantages of NULL. C ++ 11适当地定义了nullptr的属性,以避免NULL的缺点。 To summarize its properties: 总结其属性:

Property 1: it has its own type std::nullptr_t , and 属性1:它有自己的类型std::nullptr_t ,和
Property 2: it is implicitly convertible and comparable to any pointer type or pointer-to-member type, but 属性2:它是可隐式转换的,可与任何指针类型或指向成员类型的类型相媲美,但是
Property 3: it is not implicitly convertible or comparable to integral types, except for bool . 属性3:除了bool之外,它不可隐式转换或与整数类型相比。

Consider the following example: 请考虑以下示例:

#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;
}

In the above program, 在上面的程序中,

  • Case 1: OK - Property 2 案例1:好的 - 财产2
  • Case 2: Not Ok - Property 3 案例2:不好 - 财产3
  • Case 3: OK - Property 3 案例3:好的 - 财产3
  • Case 4: No confusion - Calls char * version, Property 2 & 3 案例4:没有混淆 - 调用char *版本,属性2和3

Thus introduction of nullptr avoids all the problems of good old NULL. 因此,引入nullptr避免了旧的NULL的所有问题。

How and where should you use nullptr ? 你应该如何以及在哪里使用nullptr

The rule of thumb for C++11 is simply start using nullptr whenever you would have otherwise used NULL in the past. C ++ 11的经验法则只是在过去使用NULL时开始使用nullptr


Standard References: 标准参考:

C++11 Standard: C.3.2.4 Macro NULL C ++ 11标准:C.3.2.4宏NULL
C++11 Standard: 18.2 Types C ++ 11标准:18.2类型
C++11 Standard: 4.10 Pointer conversions C ++ 11 Standard:4.10指针转换
C99 Standard: 6.3.2.3 Pointers C99标准:6.3.2.3指针

The real motivation here is perfect forwarding . 这里的真正动机是完美转发

Consider: 考虑:

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

Simply put, 0 is a special value , but values cannot propagate through the system- only types can. 简单地说,0是一个特殊 ,但值不能通过系统传播 - 只有类型可以。 Forwarding functions are essential, and 0 can't deal with them. 转发功能是必不可少的,0不能处理它们。 Thus, it was absolutely necessary to introduce nullptr , where the type is what is special, and the type can indeed propagate. 因此,绝对有必要引入nullptr ,其中类型是特殊的,并且类型确实可以传播。 In fact, the MSVC team had to introduce nullptr ahead of schedule after they implemented rvalue references and then discovered this pitfall for themselves. 实际上,MSVC团队在实施右值引用之后必须提前引入nullptr ,然后才发现这个陷阱。

There are a few other corner cases where nullptr can make life easier- but it's not a core case, as a cast can solve these problems. 还有一些其他nullptr情况,其中nullptr可以使生活更轻松 - 但它不是核心案例,因为演员可以解决这些问题。 Consider 考虑

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

Calls two separate overloads. 调用两个单独的重载。 In addition, consider 另外,考虑一下

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

This is ambiguous. 这是模棱两可的。 But, with nullptr, you can provide 但是,使用nullptr,您可以提供

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

Basics of nullptr nullp的基础知识

std::nullptr_t is the type of the null pointer literal, nullptr. std::nullptr_t是空指针文字的类型,nullptr。 It is a prvalue/rvalue of type std::nullptr_t . 它是std::nullptr_t类型的prvalue / rvalue。 There exist implicit conversions from nullptr to null pointer value of any pointer type. 存在从nullptr到任何指针类型的空指针值的隐式转换。

The literal 0 is an int, not a pointer. 文字0是int,而不是指针。 If C++ finds itself looking at 0 in a context where only a pointer can be used, it'll grudgingly interpret 0 as a null pointer, but that's a fallback position. 如果C ++在只能使用指针的上下文中发现自己看0,那么它会勉强将0解释为空指针,但这是一个后备位置。 C++'s primary policy is that 0 is an int, not a pointer. C ++的主要策略是0是int,而不是指针。

Advantage 1 - Remove ambiguity when overloading on pointer and integral types 优点1 - 在指针和整数类型上重载时消除歧义

In C++98, the primary implication of this was that overloading on pointer and integral types could lead to surprises. 在C ++ 98中,其主要含义是指针和整数类型的重载可能会导致意外。 Passing 0 or NULL to such overloads never called a pointer overload: 将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*)

The interesting thing about that call is the contradiction between the apparent meaning of the source code (“I am calling fun with NULL-the null pointer”) and its actual meaning (“I am calling fun with some kind of integer— not the null pointer”). 关于那个调用的有趣之处在于源代码的明显含义(“我用NULL调用有趣的空指针”)和它的实际含义之间的矛盾(“我用某种整数调用有趣 - 而不是null指针”)。

nullptr's advantage is that it doesn't have an integral type. nullptr的优点是它没有整数类型。 Calling the overloaded function fun with nullptr calls the void* overload (ie, the pointer overload), because nullptr can't be viewed as anything integral: 使用nullptr调用重载函数有趣会调用void * overload(即指针重载),因为nullptr不能被视为任何整数:

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

Using nullptr instead of 0 or NULL thus avoids overload resolution surprises. 使用nullptr而不是0或NULL可以避免重载决策意外。

Another advantage of nullptr over NULL(0) when using auto for return type 当auto用于返回类型时, nullptr优于NULL(0)另一个优点

For example, suppose you encounter this in a code base: 例如,假设您在代码库中遇到此问题:

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

If you don't happen to know (or can't easily find out) what findRecord returns, it may not be clear whether result is a pointer type or an integral type. 如果您碰巧知道(或不能轻易找出)findRecord返回的内容,则可能不清楚结果是指针类型还是整数类型。 After all, 0 (what result is tested against) could go either way. 毕竟,0(测试的结果是什么)可以是任何一种方式。 If you see the following, on the other hand, 另一方面,如果您看到以下内容,

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

there's no ambiguity: result must be a pointer type. 没有歧义:结果必须是指针类型。

Advantage 3 优势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;
}

Above program compile and executed successfully but lockAndCallF1, lockAndCallF2 & lockAndCallF3 have redundant code. 上面的程序编译并执行成功但是lockAndCallF1,lockAndCallF2和lockAndCallF3都有冗余代码。 It is pity to write code like this if we can write template for all these lockAndCallF1, lockAndCallF2 & lockAndCallF3 . 如果我们可以为所有这些lockAndCallF1, lockAndCallF2 & lockAndCallF3编写模板,那么编写这样的代码是很遗憾的。 So it can be generalized with template. 所以它可以用模板推广。 I have written template function lockAndCall instead of multiple definition lockAndCallF1, lockAndCallF2 & lockAndCallF3 for redundant code. 我编写了模板函数lockAndCall而不是多个定义lockAndCallF1, lockAndCallF2 & lockAndCallF3用于冗余代码。

Code is re-factored as below: 代码重新计算如下:

#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;
}

Detail analysis why compilation failed for lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr) not for lockAndCall(f3, f3m, nullptr) 详细分析为什么lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)编译失败而不是lockAndCall(f3, f3m, nullptr)

Why compilation of lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr) failed? 为什么编译lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)失败?

The problem is that when 0 is passed to lockAndCall, template type deduction kicks in to figure out its type. 问题是,当0传递给lockAndCall时,模板类型推导开始计算其类型。 The type of 0 is int, so that's the type of the parameter ptr inside the instantiation of this call to lockAndCall. 0的类型是int,因此这是对lockAndCall调用实例化中的参数ptr的类型。 Unfortunately, this means that in the call to func inside lockAndCall, an int is being passed, and that's not compatible with the std::shared_ptr<int> parameter that f1 expects. 不幸的是,这意味着在lockAndCall内部调用func时,正在传递一个int,并且它与f1期望的std::shared_ptr<int>参数不兼容。 The 0 passed in the call to lockAndCall was intended to represent a null pointer, but what actually got passed was int. 在调用lockAndCall时传递的0用于表示空指针,但实际传递的是int。 Trying to pass this int to f1 as a std::shared_ptr<int> is a type error. 尝试将此int作为std::shared_ptr<int>传递给f1是一个类型错误。 The call to lockAndCall with 0 fails because inside the template, an int is being passed to a function that requires a std::shared_ptr<int> . lockAndCall的调用失败,因为在模板内部,int被传递给需要std::shared_ptr<int>的函数。

The analysis for the call involving NULL is essentially the same. 涉及NULL的调用的分析基本相同。 When NULL is passed to lockAndCall , an integral type is deduced for the parameter ptr, and a type error occurs when ptr —an int or int-like type—is passed to f2 , which expects to get a std::unique_ptr<int> . NULL传递给lockAndCall ,会为参数ptr推导出一个整数类型,当ptr -an int或int-like lockAndCall传递给f2时会发生类型错误, f2期望得到一个std::unique_ptr<int>

In contrast, the call involving nullptr has no trouble. 相反,涉及nullptr的调用没有问题。 When nullptr is passed to lockAndCall , the type for ptr is deduced to be std::nullptr_t . nullptr传递给lockAndCallptr的类型推断为std::nullptr_t When ptr is passed to f3 , there's an implicit conversion from std::nullptr_t to int* , because std::nullptr_t implicitly converts to all pointer types. ptr传递给f3 ,存在从std::nullptr_tint*的隐式转换,因为std::nullptr_t隐式转换为所有指针类型。

It is recommended, Whenever you want to refer to a null pointer, use nullptr, not 0 or NULL . 建议,每当要引用空指针时,请使用nullptr,而不是0或NULL

There is no direct advantage of having nullptr in the way you have shown the examples. 以您显示示例的方式使用nullptr没有直接的优势。
But consider a situation where you have 2 functions with same name; 但考虑一下你有2个同名函数的情况; 1 takes int and another an int* 1取int和另一个int*

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

If you want to call foo(int*) by passing a NULL, then the way is: 如果你想通过传递NULL来调用foo(int*) ,那么方法是:

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

nullptr makes it more easy and intuitive : nullptr使它更简单直观

foo(nullptr);

Additional link from Bjarne's webpage. Bjarne网页的其他链接
Irrelevant but on C++11 side note: 不相关但在C ++ 11方面注意:

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

Just as others have already said, its primary advantage lies in overloads. 正如其他人已经说过的那样,它的主要优势在于超载。 And while explicit int vs. pointer overloads can be rare, consider standard library functions like std::fill (which has bitten me more than once in C++03): 虽然显式的int与指针重载很少见,但考虑标准的库函数,比如std::fill (它在C ++ 03中多次咬过我):

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

Doesn't compile: Cannot convert int to MyClass* . 不编译: Cannot convert int to MyClass*

IMO more important than those overload issues: in deeply nested template constructs, it's hard not to lose track of the types, and giving explicit signatures is quite an endeavour. IMO比那些超载问题更重要:在深层嵌套的模板结构中,很难不忘记类型,并且给出明确的签名是一项非常努力的工作。 So for everything that you use, the more precisely focused to the intended purpose, the better, it will reduce the need for explicit signatures and allows the compiler to produce more insightful error messages when something goes wrong. 因此,对于您使用的所有内容,越精确地集中于预期目的,就越好,它将减少对显式签名的需求,并允许编译器在出现问题时生成更具洞察力的错误消息。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM