Quais são as vantagens de usar o nullptr?

163

Conceitualmente, esse trecho de código faz a mesma coisa para os três ponteiros (inicialização segura do ponteiro):

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

E então, quais são as vantagens de atribuir ponteiros nullptrsobre a atribuição de valores NULLou 0?

Mark Garcia
fonte
39
Por um lado, uma função sobrecarregada assume inte void *não escolhe a intversão sobre a void *versão ao usar nullptr.
Chris12 /
2
Bem f(nullptr)é diferente de f(NULL). Mas no que diz respeito ao código acima (atribuindo a uma variável local), todos os três ponteiros são exatamente iguais. A única vantagem é a legibilidade do código.
221212
2
Sou a favor de fazer deste um FAQ, @Prasoon. Obrigado!
S12/
1
NB NULL não garante, historicamente, que seja 0, mas é como oc C99, da mesma maneira que um byte não tinha necessariamente 8 bits e verdadeiro e falso eram valores dependentes da arquitetura. Esta questão se concentra em nullptrbutthat é a diferença entre 0 eNULL
awiebe

Respostas:

180

Nesse código, não parece haver uma vantagem. Mas considere as seguintes funções sobrecarregadas:

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

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

Qual função será chamada? Claro, a intenção aqui é chamar f(char const *), mas na realidade f(int)será chamado! Esse é um grande problema 1 , não é?

Portanto, a solução para esses problemas é usar nullptr:

f(nullptr); //first function is called

Claro, essa não é a única vantagem de nullptr. Aqui está outro:

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

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

Como no modelo, o tipo de nullptré deduzido como nullptr_t, então você pode escrever isso:

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

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

1. Em C ++, NULLé definido como #define NULL 0, basicamente int, é por isso que f(int)é chamado.

Nawaz
fonte
1
Como o que Mehrdad havia declarado, esse tipo de sobrecarga é bem raro. Existem outras vantagens relevantes nullptr? (Não, eu não estou exigindo)
Mark Garcia
2
@MarkGarcia, Isso pode ser útil: stackoverflow.com/questions/13665349/…
chris
9
Sua nota de rodapé parece invertida. NULLé exigido pelo padrão para ter um tipo integral, e é por isso que geralmente é definido como 0ou 0L. Também não tenho certeza se gosto dessa nullptr_tsobrecarga, pois ela captura apenas chamadas com nullptr, não com um ponteiro nulo de um tipo diferente, como (void*)0. Mas posso acreditar que ele tem alguns usos, mesmo que tudo o que faça seja salvar você definindo um tipo de espaço reservado próprio para significar "nenhum".
21812 Steve Steveop
1
Outra vantagem (embora reconhecidamente menor) pode ser que nullptrtenha um valor numérico bem definido, enquanto as constantes de ponteiro nulo não. Uma constante de ponteiro nulo é convertida no ponteiro nulo desse tipo (seja o que for). É necessário que dois ponteiros nulos do mesmo tipo sejam comparados de forma idêntica, e a conversão booleana transforma um ponteiro nulo em false. Nada mais é necessário. Portanto, é possível para um compilador (bobo, mas possível) usar, por exemplo, 0xabcdef1234ou algum outro número para o ponteiro nulo. Por outro lado, nullptré necessário converter para zero numérico.
Damon
1
@DeadMG: O que está incorreto na minha resposta? que f(nullptr)não chamará a função pretendida? Havia mais de uma motivação. Muitas outras coisas úteis podem ser descobertas pelos próprios programadores nos próximos anos. Portanto, você não pode dizer que existe apenas um uso verdadeiro de nullptr.
Nawaz
87

O C ++ 11 introduz nullptr, é conhecido como Nullconstante de ponteiro e melhora a segurança do tipo e resolve situações ambíguas, diferentemente da constante de ponteiro nulo dependente da implementação existente NULL. Ser capaz de entender as vantagens de nullptr. primeiro precisamos entender o que é NULLe quais são os problemas associados a ele.


O que é NULLexatamente?

O pré C ++ 11 NULLfoi usado para representar um ponteiro que não tem valor ou ponteiro que não aponta para nada válido. Ao contrário da noção popular, NULLnão é uma palavra-chave em C ++ . É um identificador definido nos cabeçalhos da biblioteca padrão. Em resumo, você não pode usar NULLsem incluir alguns cabeçalhos de biblioteca padrão. Considere o programa de amostra :

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

Resultado:

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

O padrão C ++ define NULL como uma macro definida por implementação definida em certos arquivos de cabeçalho da biblioteca padrão. A origem de NULL é de C e C ++ a herdou de C. O padrão C definiu NULL como 0ou (void *)0. Mas em C ++ há uma diferença sutil.

C ++ não pôde aceitar esta especificação como ela é. Ao contrário de C, C ++ é uma linguagem fortemente tipada (C não requer conversão explícita de void*para qualquer tipo, enquanto C ++ exige uma conversão explícita). Isso torna a definição de NULL especificada pelo padrão C inútil em muitas expressões C ++. Por exemplo:

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

Se NULL foi definido como (void *)0, nenhuma das expressões acima funcionaria.

  • Caso 1: Não será compilado porque é necessária uma conversão automática de void *para std::string.
  • Caso 2: Não será compilado porque void *é necessário converter de para ponteiro para a função de membro.

Portanto, diferentemente de C, o C ++ Standard exigia definir NULL como literal numérico 0ou 0L.


Então, qual é a necessidade de outro constante nulo de ponteiro quando NULLjá o temos ?

Embora o comitê de padrões do C ++ tenha apresentado uma definição NULL que funcione para o C ++, essa definição teve seu próprio quinhão de problemas. NULL funcionou bem o suficiente para quase todos os cenários, mas não todos. Deu resultados surpreendentes e errôneos para certos cenários raros. Por exemplo :

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

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

Resultado:

In Int version

Claramente, a intenção parece ser chamar a versão que leva char*como argumento, mas como a saída mostra a função que leva uma intversão é chamada. Isso ocorre porque NULL é um literal numérico.

Além disso, como é definido pela implementação se NULL é 0 ou 0L, pode haver muita confusão na resolução da sobrecarga de função.

Programa de exemplo:

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

Analisando o snippet acima:

  • Caso 1: chama doSomething(char *)conforme o esperado.
  • Caso 2: chamadas, doSomething(int)mas talvez a char*versão tenha sido desejada porque 0IS também é um ponteiro nulo.
  • Caso 3: Se NULLfor definido como 0, chama doSomething(int)quando talvez doSomething(char *)pretendido, talvez resultando em erro lógico no tempo de execução. Se NULLdefinido como 0L, a chamada é ambígua e resulta em erro de compilação.

Portanto, dependendo da implementação, o mesmo código pode gerar vários resultados, o que é claramente indesejado. Naturalmente, o comitê de padrões do C ++ queria corrigir isso e essa é a principal motivação do nullptr.


Então, o que é nullptre como evita os problemas NULL?

O C ++ 11 introduz uma nova palavra nullptr- chave para servir como constante de ponteiro nulo. Ao contrário de NULL, seu comportamento não é definido pela implementação. Não é uma macro, mas tem seu próprio tipo. nullptr tem o tipo std::nullptr_t. C ++ 11 define apropriadamente propriedades para o nullptr para evitar as desvantagens de NULL. Para resumir suas propriedades:

Propriedade 1: possui seu próprio tipo std::nullptr_te
Propriedade 2: é implicitamente conversível e comparável a qualquer tipo de ponteiro ou ponteiro para membro, mas
Propriedade 3: não é implicitamente conversível ou comparável a tipos integrais, exceto bool.

Considere o seguinte exemplo:

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

No programa acima,

  • Caso 1: OK - Propriedade 2
  • Caso 2: Não aprovado - Propriedade 3
  • Caso 3: OK - Propriedade 3
  • Caso 4: Sem confusão - char *Versão de chamadas , Propriedade 2 e 3

Assim, a introdução do nullptr evita todos os problemas do bom e velho NULL.

Como e onde você deve usar nullptr?

A regra de ouro para o C ++ 11 é simplesmente começar a usar nullptrsempre que você teria usado NULL no passado.


Referências padrão:

Padrão C ++ 11: Macro C.3.2.4 NULL
Padrão C ++ 11: 18.2 Tipos
Padrão C ++ 11: 4.10 Conversões de ponteiro
Padrão C99: 6.3.2.3 Ponteiros

Alok Save
fonte
Eu já estou praticando seu último conselho desde que eu soube nullptr, embora eu não soubesse que diferença realmente faz para o meu código. Obrigado pela ótima resposta e especialmente pelo esforço. Me trouxe muita luz sobre o assunto.
Mark Garcia
"em determinados arquivos de cabeçalho da biblioteca padrão." -> por que não escrever "cstddef" desde o início?
Mxmlnkn 14/04
Por que devemos permitir que nullptr seja convertível em tipo bool? Você poderia elaborar mais?
Robert Wang
... foi usado para representar um ponteiro que não tem valor ... Variáveis sempre têm um valor. Pode ser ruído ou 0xccccc...., mas uma variável sem valor é uma contradição inerente.
3Dave
"Caso 3: OK - Propriedade 3" (linha bool flag = nullptr;). Não, tudo bem, eu recebo o seguinte erro em tempo de compilação com o g ++ 6:error: converting to ‘bool’ from ‘std::nullptr_t’ requires direct-initialization [-fpermissive]
Georg
23

A verdadeira motivação aqui é o encaminhamento perfeito .

Considerar:

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

Simplificando, 0 é um valor especial , mas os valores não podem se propagar pelos tipos somente do sistema. As funções de encaminhamento são essenciais e 0 não pode lidar com elas. Assim, era absolutamente necessário introduzir nullptr, onde o tipo é o que é especial, e o tipo pode realmente se propagar. De fato, a equipe do MSVC teve que se apresentar com nullptrantecedência depois de implementar as referências de valor e depois descobrir essa armadilha para si.

Existem alguns outros casos de canto em nullptrque a vida é mais fácil - mas não é um caso essencial, pois o elenco pode resolver esses problemas. Considerar

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

Chama duas sobrecargas separadas. Além disso, considere

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

Isso é ambíguo. Mas, com o nullptr, você pode fornecer

void f(std::nullptr_t)
int main() { f(nullptr); }
Cachorro
fonte
7
Engraçado. A metade da resposta é igual a outras duas respostas que, de acordo com você, são respostas "bastante incorretas" !!!
Nawaz
O problema de encaminhamento também pode ser resolvido com um elenco. forward((int*)0)trabalho. Estou esquecendo de algo?
Jcsahnwaldt Restabelece Monica
5

Noções básicas de nullptr

std::nullptr_té o tipo do literal de ponteiro nulo, nullptr. É um prvalue / rvalue do tipo std::nullptr_t. Existem conversões implícitas de nullptr para valor de ponteiro nulo de qualquer tipo de ponteiro.

O literal 0 é um int, não um ponteiro. Se o C ++ se encontrar olhando para 0 em um contexto onde apenas um ponteiro pode ser usado, interpretará de má vontade 0 como um ponteiro nulo, mas essa é uma posição de fallback. A política principal do C ++ é que 0 é um int, não um ponteiro.

Vantagem 1 - Remova a ambiguidade ao sobrecarregar nos tipos ponteiro e integral

No C ++ 98, a principal implicação disso era que a sobrecarga nos tipos ponteiro e integral poderia levar a surpresas. Passar 0 ou NULL a essas sobrecargas nunca chamou sobrecarga de ponteiro:

   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*)

O interessante dessa chamada é a contradição entre o significado aparente do código-fonte ("Estou chamando diversão com NULL - o ponteiro nulo") e seu significado real ("Estou chamando diversão com algum tipo de número inteiro - não o nulo ponteiro ").

A vantagem do nullptr é que ele não possui um tipo integral. A diversão da função sobrecarregada com nullptr chama a sobrecarga void * (ou seja, a sobrecarga do ponteiro), porque nullptr não pode ser visto como algo integral:

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

Usar nullptr em vez de 0 ou NULL evita surpresas na resolução de sobrecarga.

Outra vantagem de nullptrmais NULL(0)quando se usa auto para o tipo de retorno

Por exemplo, suponha que você encontre isso em uma base de código:

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

Se você não souber (ou não conseguir descobrir facilmente) o que findRecord retorna, pode não estar claro se o resultado é um tipo de ponteiro ou um tipo integral. Afinal, 0 (cujo resultado é testado) poderia ser de qualquer maneira. Se você vir o seguinte, por outro lado,

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

não há ambiguidade: o resultado deve ser do tipo ponteiro.

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

O programa acima é compilado e executado com êxito, mas lockAndCallF1, lockAndCallF2 e lockAndCallF3 têm código redundante. É uma pena escrever um código como este se pudermos escrever um modelo para tudo isso lockAndCallF1, lockAndCallF2 & lockAndCallF3. Portanto, pode ser generalizado com o modelo. Eu escrevi a função de modelo em lockAndCallvez de várias definições lockAndCallF1, lockAndCallF2 & lockAndCallF3para código redundante.

O código é re-fatorado como abaixo:

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

Análise detalhada por que a compilação falhou, lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)não paralockAndCall(f3, f3m, nullptr)

Por que a compilação de lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)falhou?

O problema é que, quando 0 é passado para lockAndCall, a dedução do tipo de modelo entra em ação para descobrir seu tipo. O tipo 0 é int, portanto esse é o tipo do parâmetro ptr dentro da instanciação dessa chamada para lockAndCall. Infelizmente, isso significa que, na chamada para funcionar dentro de lockAndCall, um int está sendo passado e isso não é compatível com o std::shared_ptr<int>parâmetro f1esperado. O 0 passado na chamada para lockAndCallera destinado a representar um ponteiro nulo, mas o que realmente foi passado foi int. Tentar passar essa int para f1 como a std::shared_ptr<int>é um erro de tipo. A chamada para lockAndCallcom 0 falha porque, dentro do modelo, um int está sendo passado para uma função que requer a std::shared_ptr<int>.

A análise da chamada envolvendo NULLé essencialmente a mesma. Quando NULLé passado para lockAndCall, um tipo integral é deduzido para o parâmetro ptr, e ocorre um erro de tipo quando ptr- um tipo int ou int-like - é passado para f2, o qual espera obter a std::unique_ptr<int>.

Por outro lado, a ligação envolvida nullptrnão apresenta problemas. Quando nullptré passado para lockAndCall, o tipo de ptré deduzido como sendo std::nullptr_t. Quando ptré passado para f3, há uma conversão implícita de std::nullptr_tpara int*, porque std::nullptr_tconverte implicitamente em todos os tipos de ponteiro.

É recomendável, sempre que você desejar se referir a um ponteiro nulo, use nullptr, não 0 ou NULL.

Ajay yadav
fonte
4

Não há vantagem direta de ter nullptrda maneira que você mostrou os exemplos.
Mas considere uma situação em que você tem 2 funções com o mesmo nome; 1 leva inte outro umint*

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

Se você deseja chamar foo(int*)passando um NULL, então o caminho é:

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

nullptrtorna mais fácil e intuitivo :

foo(nullptr);

Link adicional da página de Bjarne.
Irrelevante, mas na nota lateral do C ++ 11:

auto p = 0; // makes auto as int
auto p = nullptr; // makes auto as decltype(nullptr)
iammilind
fonte
3
Para referência, decltype(nullptr)é std::nullptr_t.
Chris12 /
2
@MarkGarcia, é um tipo completo até onde eu sei.
Chris12 /
5
@MarkGarcia, é uma pergunta interessante. cppreference tem: typedef decltype(nullptr) nullptr_t;. Eu acho que posso olhar no padrão. Ah, encontrei: Nota: std :: nullptr_t é um tipo distinto que não é um tipo de ponteiro nem um ponteiro para o tipo de membro; em vez disso, um pré-valor desse tipo é uma constante de ponteiro nulo e pode ser convertido em um valor de ponteiro nulo ou em um valor de ponteiro de membro nulo.
Chris12 /
2
@DeadMG: Havia mais de uma motivação. Muitas outras coisas úteis podem ser descobertas pelos próprios programadores nos próximos anos. Portanto, você não pode dizer que existe apenas um uso verdadeiro de nullptr.
Nawaz
2
@DeadMG: Mas você disse que esta resposta é "bastante incorreta", simplesmente porque não fala sobre "a verdadeira motivação" de que você falou na sua resposta. Não apenas que esta resposta (e a minha também) recebeu um voto negativo de você.
Nawaz
4

Assim como outros já disseram, sua principal vantagem está em sobrecargas. E embora as intsobrecargas explícitas versus as de ponteiros possam ser raras, considere as funções padrão da biblioteca, como std::fill(que me incomodou mais de uma vez no C ++ 03):

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

Não compila: Cannot convert int to MyClass*.

Angew não se orgulha mais de SO
fonte
2

A IMO é mais importante do que esses problemas de sobrecarga: em construções de modelos profundamente aninhadas, é difícil não perder o controle dos tipos e fornecer assinaturas explícitas é um grande esforço. Portanto, para tudo o que você usa, quanto mais precisamente o objetivo é o objetivo, reduzirá a necessidade de assinaturas explícitas e permitirá que o compilador produza mensagens de erro mais perspicazes quando algo der errado.

leftaroundabout
fonte