¿Cuáles son las ventajas de usar nullptr?

163

Este código conceptual hace lo mismo para los tres punteros (inicialización segura del puntero):

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

Entonces, ¿cuáles son las ventajas de asignar punteros nullptrsobre la asignación de los valores NULLo 0?

Mark Garcia
fuente
39
Por un lado, una función sobrecargada toma inty void *no elegirá la intversión sobre la void *versión cuando se usa nullptr.
Chris
2
Bueno f(nullptr)es diferente de f(NULL). Pero en lo que respecta al código anterior (asignación a una variable local), los tres punteros son exactamente iguales. La única ventaja es la legibilidad del código.
balki
2
Estoy a favor de hacer de esto una pregunta frecuente, @Prasoon. ¡Gracias!
sbi
1
NB NULL históricamente no se garantiza que sea 0, pero es como oc C99, de la misma manera que un byte no tenía necesariamente 8 bits de longitud y verdadero y falso eran valores dependientes de la arquitectura. Esta pregunta se centra en pero nullptresa es la diferencia entre 0 yNULL
awiebe

Respuestas:

180

En ese código, no parece haber una ventaja. Pero considere las siguientes funciones sobrecargadas:

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

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

¿Qué función se llamará? Por supuesto, la intención aquí es llamar f(char const *), ¡pero en realidad f(int)se llamará! Ese es un gran problema 1 , ¿no?

Entonces, la solución a tales problemas es usar nullptr:

f(nullptr); //first function is called

Por supuesto, esa no es la única ventaja de nullptr. Aquí está otro:

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

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

Como en la plantilla, el tipo de nullptrse deduce como nullptr_t, por lo que puede escribir esto:

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

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

1. En C ++, NULLse define como #define NULL 0, por lo que es básicamente int, por eso f(int)se llama.

Nawaz
fuente
1
Como había dicho Mehrdad, este tipo de sobrecargas son bastante raras. ¿Hay otras ventajas relevantes de nullptr? (No. No soy exigente)
Mark Garcia
2
@ MarkGarcia, esto podría ser útil: stackoverflow.com/questions/13665349/…
chris
9
Tu nota al pie parece al revés. NULLel estándar requiere que tenga un tipo integral, y es por eso que generalmente se define como 0o 0L. Además, no estoy seguro de que me guste esa nullptr_tsobrecarga, ya que solo captura llamadas con nullptr, no con un puntero nulo de un tipo diferente, como (void*)0. Pero puedo creer que tiene algunos usos, incluso si todo lo que hace es ahorrarte definiendo un tipo de marcador de posición de un solo valor para que signifique "ninguno".
Steve Jessop
1
Otra ventaja (aunque ciertamente menor) puede ser que nullptrtiene un valor numérico bien definido, mientras que las constantes de puntero nulo no. Una constante de puntero nulo se convierte en el puntero nulo de ese tipo (sea lo que sea). Se requiere que dos punteros nulos del mismo tipo se comparen de manera idéntica, y la conversión booleana convierte un puntero nulo en false. No se requiere nada más. Por lo tanto, es posible que un compilador (tonto, pero posible) utilice, por ejemplo, 0xabcdef1234o algún otro número para el puntero nulo. Por otro lado, nullptrse requiere convertir a cero numérico.
Damon
1
@DeadMG: ¿Qué es incorrecto en mi respuesta? que f(nullptr)no llamará a la función prevista? Hubo más de una motivación. Los programadores pueden descubrir muchas otras cosas útiles en los próximos años. Por lo tanto, no puede decir que solo hay un uso verdadero de nullptr.
Nawaz
87

C ++ 11 introduce nullptr, se conoce como la Nullconstante de puntero y mejora la seguridad de tipo y resuelve situaciones ambiguas a diferencia de la constante de puntero nulo dependiente de la implementación existente NULL. Para poder comprender las ventajas de nullptr. primero tenemos que entender qué es NULLy cuáles son los problemas asociados con él.


¿Qué es NULLexactamente?

Pre C ++ 11 NULLse usó para representar un puntero que no tiene valor o un puntero que no apunta a nada válido. Contrariamente a la noción popular, NULLno es una palabra clave en C ++ . Es un identificador definido en los encabezados de la biblioteca estándar. En resumen, no puede usar NULLsin incluir algunos encabezados de biblioteca estándar. Considere el programa de muestra :

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

Salida:

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

El estándar C ++ define NULL como una macro definida por la implementación definida en ciertos archivos de encabezado de biblioteca estándar. El origen de NULL es de C y C ++ lo heredó de C. El estándar C definió NULL como 0o (void *)0. Pero en C ++ hay una sutil diferencia.

C ++ no pudo aceptar esta especificación tal como es. A diferencia de C, C ++ es un lenguaje fuertemente tipado (C no requiere conversión explícita de void*ningún tipo, mientras que C ++ exige una conversión explícita). Esto hace que la definición de NULL especificada por el estándar C sea inútil en muchas expresiones C ++. Por ejemplo:

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

Si NULL se definió como (void *)0, ninguna de las expresiones anteriores funcionaría.

  • Caso 1: no se compilará porque se necesita una conversión automática de void *a std::string.
  • Caso 2: No se compilará porque void *se necesita la función de conversión de puntero a miembro.

Entonces, a diferencia de C, C ++ Standard tiene el mandato de definir NULL como literal numérico 0o 0L.


Entonces, ¿cuál es la necesidad de otro puntero nulo constante cuando ya lo tenemos NULL?

Aunque el comité de estándares de C ++ propuso una definición NULL que funcionará para C ++, esta definición tenía su propia cuota de problemas. NULL funcionó lo suficientemente bien para casi todos los escenarios, pero no para todos. Dio resultados sorprendentes y erróneos para ciertos escenarios raros. Por ejemplo :

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

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

Salida:

In Int version

Claramente, la intención parece ser llamar a la versión que toma char*como argumento, pero a medida que el resultado muestra la función que toma una intversión se llama. Esto se debe a que NULL es un literal numérico.

Además, dado que está definido por la implementación si NULL es 0 o 0L, puede haber mucha confusión en la resolución de sobrecarga de funciones.

Programa de muestra:

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

Analizando el fragmento anterior:

  • Caso 1: llamadas doSomething(char *)como se esperaba.
  • Caso 2: llamadas doSomething(int)pero tal vez char*se deseaba la versión porque 0también es un puntero nulo.
  • Caso 3: Si NULLse define como 0, llama doSomething(int)cuando tal vez doSomething(char *)fue intencionado, lo que tal vez resulte en un error lógico en tiempo de ejecución. Si NULLse define como 0L, la llamada es ambigua y genera un error de compilación.

Entonces, dependiendo de la implementación, el mismo código puede dar varios resultados, lo cual es claramente indeseable. Naturalmente, el comité de estándares de C ++ quería corregir esto y esa es la principal motivación para nullptr.


Entonces, ¿qué es nullptry cómo evita los problemas NULL?

C ++ 11 introduce una nueva palabra clave nullptrpara servir como puntero nulo constante. A diferencia de NULL, su comportamiento no está definido por la implementación. No es una macro pero tiene su propio tipo. nullptr tiene el tipo std::nullptr_t. C ++ 11 define adecuadamente las propiedades de nullptr para evitar las desventajas de NULL. Para resumir sus propiedades:

Propiedad 1: tiene su propio tipo std::nullptr_t, y
Propiedad 2: es implícitamente convertible y comparable a cualquier tipo de puntero o tipo puntero a miembro, pero
Propiedad 3: no es implícitamente convertible o comparable a los tipos integrales, a excepción de bool.

Considere el siguiente ejemplo:

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

En el programa anterior,

  • Caso 1: OK - Propiedad 2
  • Caso 2: No está bien - Propiedad 3
  • Caso 3: OK - Propiedad 3
  • Caso 4: Sin confusión - char *Versión de llamadas , Propiedad 2 y 3

Por lo tanto, la introducción de nullptr evita todos los problemas del buen viejo NULL.

¿Cómo y dónde debes usar nullptr?

La regla general para C ++ 11 es simplemente comenzar a usar nullptrsiempre que de otro modo hubiera usado NULL en el pasado.


Referencias estándar:

C ++ 11 Estándar: C.3.2.4 Macro NULL
C ++ 11 Estándar: 18.2 Tipos
C ++ 11 Estándar: 4.10 Conversiones de puntero
C99 Estándar: 6.3.2.3 Punteros

Alok Save
fuente
Ya estoy practicando su último consejo desde que lo supe nullptr, aunque no sabía qué diferencia realmente tiene para mi código. Gracias por la gran respuesta y especialmente por el esfuerzo. Me trajo mucha luz sobre el tema.
Mark Garcia
"en ciertos archivos de encabezado de biblioteca estándar". -> ¿por qué no simplemente escribes "cstddef" desde el principio?
mxmlnkn
¿Por qué deberíamos permitir que nullptr sea convertible a tipo bool? ¿Podría por favor elaborar más?
Robert Wang
... se usó para representar un puntero que no tiene valor ... Las variables siempre tienen un valor. Puede ser ruido o 0xccccc...., pero, una variable sin valor es una contradicción inherente.
3Dave
"Caso 3: OK - Propiedad 3" (línea bool flag = nullptr;). No, no está bien, aparece el siguiente error en el momento de la compilación con g ++ 6:error: converting to ‘bool’ from ‘std::nullptr_t’ requires direct-initialization [-fpermissive]
Georg
23

La verdadera motivación aquí es el reenvío perfecto .

Considerar:

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

En pocas palabras, 0 es un valor especial , pero los valores no pueden propagarse a través del sistema, solo los tipos pueden. Las funciones de reenvío son esenciales y 0 no puede ocuparse de ellas. Por lo tanto, era absolutamente necesario introducir nullptr, donde el tipo es lo que es especial, y el tipo de hecho puede propagarse. De hecho, el equipo de MSVC tuvo que presentar nullptrantes de lo programado después de que implementaron referencias de valor y luego descubrieron esta trampa por sí mismos.

Hay algunos otros casos de esquina en los que nullptrpuede facilitar la vida, pero no es un caso central, ya que un elenco puede resolver estos problemas. Considerar

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

Llama a dos sobrecargas separadas. Además, considere

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

Esto es ambiguo. Pero, con nullptr, puede proporcionar

void f(std::nullptr_t)
int main() { f(nullptr); }
Perrito
fuente
77
Gracioso. ¡La mitad de la respuesta es la misma que las otras dos respuestas que según usted son respuestas "bastante incorrectas" !
Nawaz
El problema de reenvío también se puede resolver con un yeso. forward((int*)0)trabajos. ¿Me estoy perdiendo de algo?
jcsahnwaldt Restablece a Monica el
5

Conceptos básicos de nullptr

std::nullptr_tes el tipo del puntero nulo literal, nullptr. Es un valor / valor de tipo std::nullptr_t. Existen conversiones implícitas de nullptr a valor de puntero nulo de cualquier tipo de puntero.

El 0 literal es un int, no un puntero. Si C ++ se encuentra mirando a 0 en un contexto donde solo se puede usar un puntero, interpretará a regañadientes 0 como un puntero nulo, pero esa es una posición alternativa. La política principal de C ++ es que 0 es un int, no un puntero.

Ventaja 1: elimine la ambigüedad al sobrecargar el puntero y los tipos integrales

En C ++ 98, la implicación principal de esto fue que la sobrecarga en punteros y tipos integrales podría generar sorpresas. Pasar 0 o NULL a tales sobrecargas nunca se llamó sobrecarga de puntero:

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

Lo interesante de esa llamada es la contradicción entre el significado aparente del código fuente ("Estoy llamando divertido con NULL-el puntero nulo") y su significado real ("Estoy llamando divertido con algún tipo de entero, no el nulo puntero").

La ventaja de nullptr es que no tiene un tipo integral. Llamar divertido a la función sobrecargada con nullptr llama a la sobrecarga void * (es decir, la sobrecarga del puntero), porque nullptr no puede verse como algo integral:

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

Usar nullptr en lugar de 0 o NULL evita las sorpresas de resolución de sobrecarga.

Otra ventaja de nullptrsobre NULL(0)cuando se usa auto para el tipo de retorno

Por ejemplo, suponga que encuentra esto en una base de código:

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

Si no sabe (o no puede descubrir fácilmente) qué devuelve findRecord, puede que no esté claro si el resultado es un tipo de puntero o un tipo integral. Después de todo, 0 (con qué resultado se prueba) podría ir en cualquier dirección. Si ve lo siguiente, por otro lado,

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

no hay ambigüedad: el resultado debe ser un tipo de puntero.

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

El programa anterior se compila y ejecuta con éxito, pero lockAndCallF1, lockAndCallF2 y lockAndCallF3 tienen código redundante. Es una pena escribir código como este si podemos escribir una plantilla para todo esto lockAndCallF1, lockAndCallF2 & lockAndCallF3. Por lo tanto, se puede generalizar con plantilla. He escrito la función de plantilla en lockAndCalllugar de la definición múltiple lockAndCallF1, lockAndCallF2 & lockAndCallF3para código redundante.

El código se refactoriza de la siguiente manera:

#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álisis detallado de por qué la compilación falló para lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)no paralockAndCall(f3, f3m, nullptr)

¿Por qué la compilación de lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)fallido?

El problema es que cuando se pasa 0 a lockAndCall, la deducción de tipo de plantilla entra en acción para descubrir su tipo. El tipo de 0 es int, entonces ese es el tipo del parámetro ptr dentro de la instanciación de esta llamada a lockAndCall. Desafortunadamente, esto significa que en la llamada a func dentro de lockAndCall, se pasa un int, y eso no es compatible con el std::shared_ptr<int>parámetro que f1espera. El 0 pasado en la llamada a lockAndCallestaba destinado a representar un puntero nulo, pero lo que realmente pasó fue int. Intentar pasar este int a f1 como a std::shared_ptr<int>es un error de tipo. La llamada a lockAndCallcon 0 falla porque dentro de la plantilla, se pasa un int a una función que requiere a std::shared_ptr<int>.

El análisis para la llamada que involucra NULLes esencialmente el mismo. Cuando NULLse pasa a lockAndCall, se deduce un tipo integral para el parámetro ptr, y se produce un error de tipo cuando se pasa a un tipo ptrint o tipo int f2, que espera obtener a std::unique_ptr<int>.

En contraste, la llamada que involucra nullptrno tiene problemas. Cuando nullptrse pasa a lockAndCall, el tipo para ptrse deduce que es std::nullptr_t. Cuando ptrse pasa a f3, hay una conversión implícita de std::nullptr_ta int*, porque se std::nullptr_tconvierte implícitamente a todos los tipos de puntero.

Se recomienda, siempre que desee hacer referencia a un puntero nulo, use nullptr, no 0 o NULL.

Ajay yadav
fuente
4

No hay una ventaja directa de tener nullptren la forma en que ha mostrado los ejemplos.
Pero considere una situación en la que tenga 2 funciones con el mismo nombre; 1 toma inty otro unint*

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

Si desea llamar foo(int*)pasando un NULL, entonces la forma es:

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

nullptrlo hace más fácil e intuitivo :

foo(nullptr);

Enlace adicional desde la página web de Bjarne.
Irrelevante pero en C ++ 11 nota al margen:

auto p = 0; // makes auto as int
auto p = nullptr; // makes auto as decltype(nullptr)
iammilind
fuente
3
Como referencia, decltype(nullptr)es std::nullptr_t.
Chris
2
@ MarkGarcia, es un tipo completo hasta donde yo sé.
Chris
55
@ MarkGarcia, es una pregunta interesante. cppreference tiene: typedef decltype(nullptr) nullptr_t;. Supongo que puedo mirar en el estándar. Ah, lo encontré: Nota: std :: nullptr_t es un tipo distinto que no es un tipo de puntero ni un puntero al tipo de miembro; más bien, un valor de este tipo es una constante de puntero nulo y puede convertirse en un valor de puntero nulo o un valor de puntero miembro nulo.
Chris
2
@DeadMG: Hubo más de una motivación. Los programadores pueden descubrir muchas otras cosas útiles en los próximos años. Por lo tanto, no puede decir que solo hay un uso verdadero de nullptr.
Nawaz
2
@DeadMG: Pero usted dijo que esta respuesta es "bastante incorrecta" simplemente porque no habla de "la verdadera motivación" de la que habló en su respuesta. No solo que esta respuesta (y la mía también) recibió un voto negativo de usted.
Nawaz
4

Tal como otros ya han dicho, su principal ventaja radica en las sobrecargas. Y aunque las intsobrecargas explícitas frente a punteros pueden ser raras, considere las funciones de biblioteca estándar como std::fill(que me ha mordido más de una vez en C ++ 03):

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

No se compila: Cannot convert int to MyClass*.

Angew ya no está orgulloso de SO
fuente
2

OMI más importante que esos problemas de sobrecarga: en construcciones de plantillas profundamente anidadas, es difícil no perder de vista los tipos, y dar firmas explícitas es un gran esfuerzo. Por lo tanto, para todo lo que use, cuanto más enfocado con precisión al propósito previsto, mejor, reducirá la necesidad de firmas explícitas y permitirá que el compilador produzca mensajes de error más perspicaces cuando algo salga mal.

a la izquierda
fuente