繁体   English   中英

使用nullptr有什么好处?

[英]What are the advantages of using nullptr?

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

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

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

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

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)原因。

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


什么是NULL

Pre C ++ 11 NULL用于表示没有值的指针或指向无效的指针。 与流行的概念相反, NULL不是C ++中的关键字 它是标准库头中定义的标识符。 简而言之,如果不包含一些标准库头,则不能使用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标准指定的NULL定义在许多C ++表达式中无用。 例如:

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

如果将NULL定义为(void *)0 ,则上述表达式都不起作用。

  • 情况1:无法编译,因为从void *std::string需要自动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 IS也是空指针。
  • 情况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:好的 - 财产2
  • 案例2:不好 - 财产3
  • 案例3:好的 - 财产3
  • 案例4:没有混淆 - 调用char *版本,属性2和3

因此,引入nullptr避免了旧的NULL的所有问题。

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

C ++ 11的经验法则只是在过去使用NULL时开始使用nullptr


标准参考:

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

这里的真正动机是完美转发

考虑:

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情况,其中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); }

nullp的基础知识

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

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

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

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

当auto用于返回类型时, 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)失败?

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

涉及NULL的调用的分析基本相同。 NULL传递给lockAndCall ,会为参数ptr推导出一个整数类型,当ptr -an int或int-like lockAndCall传递给f2时会发生类型错误, f2期望得到一个std::unique_ptr<int>

相反,涉及nullptr的调用没有问题。 nullptr传递给lockAndCallptr的类型推断为std::nullptr_t ptr传递给f3 ,存在从std::nullptr_tint*的隐式转换,因为std::nullptr_t隐式转换为所有指针类型。

建议,每当要引用空指针时,请使用nullptr,而不是0或NULL

以您显示示例的方式使用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网页的其他链接
不相关但在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比那些超载问题更重要:在深层嵌套的模板结构中,很难不忘记类型,并且给出明确的签名是一项非常努力的工作。 因此,对于您使用的所有内容,越精确地集中于预期目的,就越好,它将减少对显式签名的需求,并允许编译器在出现问题时生成更具洞察力的错误消息。

暂无
暂无

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

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