使用 nullptr 的优点是什么?

这段代码 概念上对三个指针(安全指针初始化)执行同样的操作:

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

那么,分配指针 nullptr比分配值 NULL0有什么优势呢?

58631 次浏览

在这段代码中,似乎没有什么优势,但是考虑一下下面的重载函数:

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)被调用的原因。

在示例中使用 nullptr并没有直接的优势。
但是考虑这样一种情况: 有两个函数具有相同的名称; 1接受 int,另一个接受 int*

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

如果您想通过传递一个 NULL 来调用 foo(int*),那么方法是:

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)

正如其他人已经说过的,它的主要优势在于超载。虽然显式的 int和指针重载很少见,但是考虑一下像 std::fill这样的标准库函数(在 C + + 03中我不止一次遇到过这种情况) :

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

无法编译: Cannot convert int to MyClass*

IMO 比那些超载问题更重要: 在深度嵌套的模板构造中,很难不失去对类型的跟踪,并且给出显式签名是一项相当大的努力。因此,对于您使用的任何东西,越是精确地集中到预期的目的,就越好,它将减少对显式签名的需要,并允许编译器在出错时产生更有见地的错误消息。

真正的动机是 完美的转发

考虑一下:

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

C + + 11引入了 nullptr,它被称为 Null指针常数,它与 改善类型安全解决模棱两可的情况不同,现有的实现依赖于空指针常数 NULL。能够理解 nullptr的优点。我们首先需要了解什么是 NULL以及与它相关的问题是什么。


NULL到底是什么?

Pre C + + 11 NULL用于表示没有值的指针或指向无效内容的指针。与流行的概念 在 C + + 中,NULL不是关键字相反。它是在标准库标头中定义的标识符。简而言之,如果不包含一些标准库头,就不能使用 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 + + 标准要求将 NULL 定义为数字字面值 00L


那么,当我们已经有 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: 调用 doSomething(int),但可能需要 char*版本,因为 0也是空指针。
  • 案例3: 如果 NULL被定义为 0,那么当 doSomething(char *)可能是有意的时候调用 doSomething(int),可能在运行时导致逻辑错误。如果将 NULL定义为 0L,则调用不明确并导致编译错误。

因此,根据实现的不同,相同的代码可以产生不同的结果,这显然是不希望看到的。当然,C + + 标准委员会想要纠正这一点,这也是 nullptr 的主要动机。


那么什么是 nullptr,它如何避免 NULL的问题?

C + + 11引入了一个新的关键字 nullptr作为空指针常量。与 NULL 不同,它的行为不是实现定义的。它不是宏,但有自己的类型。Nullptr 的类型为 std::nullptr_t。C + + 11适当地为 nullptr 定义了属性,以避免 NULL 的缺点。总结其特性:

属性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: OK-属性2
  • 案例2: Not Ok-财产3
  • 案例3: OK-属性3
  • 案例4: 没有混淆-调用 char *版本,属性2和3

因此,引入 nullptr 可以避免老 NULL 的所有问题。

如何以及在哪里应该使用 nullptr

C + + 11的经验法则是,只要您在过去使用 NULL,就可以开始使用 nullptr


标准参考文献:

C + + 11标准: C 3.2.4宏为空
C + + 11标准: 18.2类型
C + + 11标准: 4.10指针转换
C99标准: 6.3.2.3指针

Nullptr 的基础知识

std::nullptr_t是空指针文字 nullptr 的类型。它是 std::nullptr_t类型的 prvalue/rvalue。存在从 nullptr 到任何指针类型的空指针值的隐式转换。

文字0是一个 int,而不是一个指针。如果 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 调用 fun ——空指针”)和它的实际意义(“我用某种整数调用 fun ——而不是空指针”)之间的矛盾。

Nullptr 的优点是它没有整型。 使用 nullptr 调用重载函数 fun 会调用 void * 重载(即指针重载) ,因为 nullptr 不能被视为任何积分:

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

使用 nullptr 而不是0或 NULL 可以避免出现重载分辨率意外。

使用 auto 作为返回类型时,nullptr优于 NULL(0)的另一个优点是

例如,假设您在代码库中遇到这种情况:

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

如果您碰巧不知道(或者不容易找到) findRecord 返回什么,那么可能不清楚 result 是指针类型还是整数类型。毕竟,0(测试的结果)可以是任何一种方式。另一方面,如果你看到以下内容,

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

没有歧义: result 必须是指针类型。

优势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)失败了?

问题在于,当0被传递给 lockAndCall 时,模板类型推断就会启动,从而计算出它的类型。0的类型是 int,因此这是这个 lockAndCall 调用的实例化中的参数 ptr 的类型。不幸的是,这意味着在 lockAndCall 内对 func 的调用中,传递了一个 int,这与 f1期望的 std::shared_ptr<int>参数不兼容。在对 lockAndCall的调用中传递的0表示空指针,但实际传递的是 int。尝试将这个 int 作为 std::shared_ptr<int>传递给 f1是一个类型错误。以0调用 lockAndCall失败,因为在模板内部,一个 int 被传递给一个需要 std::shared_ptr<int>的函数。

对于涉及 NULL的呼叫的分析基本上是相同的。当 NULL传递给 lockAndCall时,将为 ptr 参数推导出一个整数类型,当 ptrーー一个 int 或类 int 类型ーー传递给 f2时,将发生类型错误,f2期望得到一个 std::unique_ptr<int>

相比之下,涉及 nullptr的呼叫没有问题。当 nullptr传递给 lockAndCall时,ptr的类型被推断为 std::nullptr_t。当 ptr传递给 f3时,会有一个从 std::nullptr_tint*的隐式转换,因为 std::nullptr_t隐式转换为所有指针类型。

建议在需要引用空指针时,使用 nullptr,而不是0或 NULL