使用nullptr有什么好处?

时间:2012-12-11 08:35:11

标签: c++ c++11 null c++-faq nullptr

这段代码概念上对三个指针(安全指针初始化)做同样的事情:

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

那么,分配指针nullptr而不是为它们分配值NULL0有什么好处?

6 个答案:

答案 0 :(得分:170)

在该代码中,似乎没有优势。但请考虑以下重载函数:

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 :(得分: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团队在实施右值引用之后必须提前引入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); }

答案 2 :(得分:5)

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调用有趣的空指针”)和它的实际含义(“我用某种整数调用有趣的东西”之间的矛盾 - 不是空指针“)。

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

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

使用nullptr而不是0或NULL可以避免重载决策意外。

使用自动返回类型时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&amp; 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,这与std::shared_ptr<int>期望的f1参数不兼容。调用lockAndCall时传递的0用于表示空指针,但实际传递的是int。尝试将此int作为std::shared_ptr<int>传递给f1是类型错误。使用0调用lockAndCall失败,因为在模板中,int被传递给需要std::shared_ptr<int>的函数。

涉及NULL的电话的分析基本相同。当NULL传递给lockAndCall时,会为参数ptr推导出一个整数类型,并且当ptr - 一个int或类似int的类型 - 传递给{{}时会发生类型错误1}},期望获得f2

相反,涉及std::unique_ptr<int>的电话没有问题。将nullptr传递给nullptr时,lockAndCall的类型推断为ptr。将std::nullptr_t传递给ptr时,存在从f3std::nullptr_t的隐式转换,因为int*会隐式转换为所有指针类型。

建议,每当您想引用空指针时,请使用nullptr,而不是0或std::nullptr_t

答案 3 :(得分:4)

在您展示示例的方式中使用nullptr没有直接的好处 但考虑一下你有2个同名函数的情况; 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网页的

Additional link 不相关但在C ++ 11方面注意:

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

答案 4 :(得分:4)

正如其他人已经说过的那样,它的主要优势在于超载。虽然显式int与指针重载相比很少见,但请考虑标准库函数,例如std::fill(在C ++ 03中多次咬过我):

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

不编译:{{1​​}}。

答案 5 :(得分:2)

IMO比那些超载问题更重要:在深层嵌套的模板构造中,很难不忘记类型,并且给出明确的签名是一项非常努力的工作。因此,对于您使用的所有内容,越精确地集中于预期目的,就越好,它将减少对显式签名的需求,并允许编译器在出现问题时生成更具洞察力的错误消息。