¿Cuáles son las ventajas de usar nullptr?

Esta pieza de código conceptualmente hace lo mismo con 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 nullptr sobre asignarles los valores NULL o 0 ?

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 a f(char const *) , ¡pero en realidad se llamará f(int) ! 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 struct something{}; //primary template template<> struct something{}; //partial specialization for nullptr 

Dado que en la plantilla, el tipo de nullptr se deduce como nullptr_t , por lo que puede escribir esto:

 template void f(T *ptr); //function to handle non-nullptr argument void f(nullptr_t); //an overload to handle nullptr argument!!! 

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

C ++ 11 introduce nullptr , se lo conoce como la constante de puntero Null 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 entender las ventajas de nullptr . primero tenemos que entender qué es NULL y cuáles son los problemas asociados con él.


¿Qué es NULL exactamente?

Pre C ++ 11 NULL se usó para representar un puntero que no tiene ningún valor o puntero que no apunte a nada válido. Al contrario de la noción popular, NULL no es una palabra clave en C ++ . Es un identificador definido en los encabezados de biblioteca estándar. En resumen, no puede usar NULL sin incluir algunos encabezados de biblioteca estándar. Considere el progtwig de ejemplo :

 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 de C ++ define NULL como una macro definida por la implementación definida en ciertos archivos de cabecera de biblioteca estándar. El origen de NULL proviene de C y C ++ lo heredó de C. El estándar C definió NULL como 0 o (void *)0 . Pero en C ++ hay una diferencia sutil.

C ++ no podría 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* a ningún tipo, mientras que C ++ exige un reparto explícito). Esto hace que la definición de NULL especificado por el estándar C sea inútil en muchas expresiones de 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 comstackrá porque se necesita un lanzamiento automático de void * a std::string .
  • Caso 2: No se comstackrá porque se necesita lanzar desde el void * al puntero a la función miembro.

Por lo tanto, a diferencia de C, el estándar de C ++ obliga a definir NULL como literal numérico 0 o 0L .


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

Aunque el comité de estándares de C ++ creó una definición NULL que funcionará para C ++, esta definición tuvo su propia cantidad de problemas. NULL funcionó bastante bien para casi todos los escenarios pero no para todos. Dio resultados sorprendentes y erróneos para ciertos escenarios raros. Por ejemplo :

 #include 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 como el resultado muestra la función que toma una versión int 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 la función.

Progtwig de muestra:

 #include  void doSomething(int); void doSomething(char *); int main() { doSomething(static_cast (0)); // Case 1 doSomething(0); // Case 2 doSomething(NULL) // Case 3 } 

Analizando el fragmento de arriba:

  • Caso 1: llama a doSomething(char *) como se esperaba.
  • Caso 2: llama a doSomething(int) pero quizás se desee la versión char* porque 0 IS también es un puntero nulo.
  • Caso 3: Si NULL se define como 0 , llama a doSomething(int) cuando quizás se pretendía hacer algo doSomething(char *) , lo que podría ocasionar un error de lógica en el tiempo de ejecución. Si NULL se define como 0L , la llamada es ambigua y da como resultado un error de comstackción.

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


Entonces, ¿qué es nullptr y cómo evita los problemas de NULL ?

C ++ 11 introduce una nueva palabra clave nullptr para servir como constante de puntero nulo. 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 apropiadamente propiedades para el 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 de 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 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 progtwig anterior,

  • Caso 1: OK - Propiedad 2
  • Caso 2: No está bien - Propiedad 3
  • Caso 3: OK - Propiedad 3
  • Caso 4: sin confusión - Llamada a la versión de char * , propiedad 2 y 3

Por lo tanto, la introducción de nullptr evita todos los problemas del antiguo y bueno NULL.

¿Cómo y dónde debería usar nullptr ?

La regla de oro para C ++ 11 es simplemente comenzar a usar nullptr siempre que de otra manera hubiera usado NULL en el pasado.


Referencias estándar:

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

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

Considerar:

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

En pocas palabras, 0 es un valor especial, pero los valores no se pueden propagar 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 puede propagarse. De hecho, el equipo de MSVC tuvo que introducir nullptr antes de lo previsto después de implementar referencias de valor real y luego descubrieron esta trampa por sí mismos.

Hay algunos otros casos de esquina donde nullptr puede hacer la vida más fácil, 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); } 

Conceptos básicos de nullptr

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

El literal 0 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: eliminar la ambigüedad cuando se sobrecarga en el puntero y los tipos integrales

En C ++ 98, la principal consecuencia de esto fue que la sobrecarga en el puntero y los tipos integrales podría llevar a sorpresas. Al pasar 0 o NULL a tales sobrecargas nunca se llamó sobrecarga de un 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 a la diversión con NULL-el puntero nulo”) y su significado real (“Estoy llamando a la diversión con algún tipo de número entero- no el nulo” puntero”).

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

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

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

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

Por ejemplo, supongamos que encuentras esto en una base de código:

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

Si no sabe (o no puede encontrar fácilmente) lo que findRecord devuelve, puede no quedar claro si el resultado es un tipo de puntero o un tipo integral. Después de todo, 0 (qué resultado se prueba contra) 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 #include  #include  #include  using namespace std; int f1(std::shared_ptr spw) // call these only when { //do something return 0; } double f2(std::unique_ptr 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; void lockAndCallF1() { MuxtexGuard g(f1m); // lock mutex for f1 auto result = f1(static_cast(0)); // pass 0 as null ptr to f1 cout<< result<(NULL)); // pass NULL as null ptr to f2 cout<< result< 

El progtwig anterior comstack y ejecuta con éxito pero lockAndCallF1, lockAndCallF2 y lockAndCallF3 tienen código redundante. Es una lástima escribir un código como este si podemos escribir una plantilla para todos estos lockAndCallF1, lockAndCallF2 & lockAndCallF3 . Por lo tanto, se puede generalizar con plantilla. He escrito la función de plantilla lockAndCall lugar de la definición múltiple lockAndCallF1, lockAndCallF2 & lockAndCallF3 para el código redundante.

El código se vuelve a factorizar de la siguiente manera:

 #include #include  #include  #include  using namespace std; int f1(std::shared_ptr spw) // call these only when { //do something return 0; } double f2(std::unique_ptr 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; template 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); //comstacktion failed //do something auto result2 = lockAndCall(f2, f2m, NULL); //comstacktion failed //do something auto result3 = lockAndCall(f3, f3m, nullptr); //do something return 0; } 

Análisis detallado de por qué la comstackción falló para lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr) no para lockAndCall(f3, f3m, nullptr)

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

El problema es que cuando 0 pasa a lockAndCall, la deducción de tipo de plantilla entra en acción para descubrir su tipo. El tipo de 0 es int, por lo que 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 está pasando una int, y eso no es compatible con el parámetro std::shared_ptr que f1 espera. El 0 pasado en la llamada a lockAndCall estaba destinado a representar un puntero nulo, pero lo que realmente pasó fue int. Intentar pasar este int a f1 como un std::shared_ptr es un tipo de error. La llamada a lockAndCall con 0 falla porque dentro de la plantilla, se está pasando una int a una función que requiere un std::shared_ptr .

El análisis para la llamada que implica NULL es esencialmente el mismo. Cuando se pasa NULL a lockAndCall , se deduce un tipo integral para el parámetro ptr, y se produce un error de tipo cuando se ptr -un int o int-like type-a f2 , que espera obtener un std::unique_ptr .

Por el contrario, la llamada que implica nullptr no tiene problemas. Cuando se pasa nullptr a lockAndCall , se deduce que el tipo para ptr es std::nullptr_t . Cuando ptr se pasa a f3 , hay una conversión implícita de std::nullptr_t a int* , porque std::nullptr_t convierte implícitamente a todos los tipos de puntero.

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

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

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

Si quieres llamar a foo(int*) pasando un NULL, entonces el camino es:

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

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

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

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

No comstack: Cannot convert int to MyClass* .

IMO es más importante que esos problemas de sobrecarga: en construcciones de plantilla profundamente anidadas, es difícil no perder de vista los tipos, y dar firmas explícitas es todo un empeño. Por lo tanto, para todo lo que utiliza, cuanto más preciso sea el objective, mejor reducirá la necesidad de firmas explícitas y permitirá al comstackdor emitir mensajes de error más perspicaces cuando algo vaya mal.