¿Cuáles son las ventajas de usar nullptr?


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

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

Y entonces, ¿cuáles son las ventajas de asignar punteros nullptr sobre asignarles los valores NULL o 0?

Author: Visruth, 2012-12-11

7 answers

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) será llamado! Ese es un gran problema1, ¿no lo es?

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í hay otro:

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

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

Puesto que en la plantilla, el el tipo de nullptr se 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++, NULL se define como #define NULL 0, por lo que es básicamente int, por eso se llama f(int).

 163
Author: Nawaz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-29 09:15:52

C++11 introduce nullptr, se conoce como la constante de puntero Null y mejora la seguridad del 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 necesitamos entender qué es NULL y cuáles son los problemas asociados con él.


¿Qué es NULL exactamente?

Pre C++11 NULL se utilizó para representar un puntero que no tiene valor o puntero que no apunta a nada válido. Contrario a la noción popular NULL no es una palabra clave en C++. Es un identificador definido en encabezados de biblioteca estándar. En resumen, no se puede usar NULL sin incluir algunas cabeceras de biblioteca estándar. Considere la Programa 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 cierto estándar archivos de cabecera de biblioteca. El origen de NULL es de C y C++ lo heredó de C. El estándar de C definió NULL como 0 o (void *)0. Pero en C++ hay una diferencia sutil.

C++ no pudo aceptar esta especificación tal como está. A diferencia de C, C++ es un lenguaje fuertemente escrito (C no requiere un cast explícito de void* a ningún tipo, mientras que C++ exige un cast explícito). Esto hace que la definición de NULL especificada por C standard 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 compilará porque se necesita un cast automático de void * a std::string.
  • Caso 2: No se compilará porque se necesita la función cast from void * to pointer to member.

Así que a diferencia de C, el estándar C++ requiere definir NULL como literal numérico 0 o 0L.


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

Aunque al comité de Estándares de C++ se le ocurrió una definición NULA que funcionará para C++, esta definición tenía su propia parte de problemas. NULL funcionó lo suficientemente bien para casi todos los escenarios, pero no 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 el argumento, pero como muestra la salida, se llama a la función que toma una versión int. Esto es porque 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.

Ejemplo de programa:

#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 de código anterior:

  • Caso 1: llama a doSomething(char *) como se esperaba.
  • Caso 2: llamadas doSomething(int) pero tal vez char* se deseaba la versión porque 0 también es un puntero nulo.
  • Caso 3: Si NULL se define como 0, llama a doSomething(int) cuando quizás se pretendía doSomething(char *), lo que puede resultar en un error lógico en tiempo de ejecución. Si NULL se define como 0L, la llamada es ambigua y resulta en un error de compilación.

Entonces, dependiendo de la implementación, el mismo código puede dar varios resultados, lo cual es claramente indeseado. Naturalmente, el comité de estándares de C++ quería corregir esto y que es la motivación principal 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 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 las propiedades para el nullptr para evitar las desventajas de NULL. Para resumir su 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 puntero a miembro, pero
Propiedad 3: no es implícitamente convertible o comparable a los tipos integrales, excepto para bool.

Considere el siguiente ejemplo:{[56]]}

#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-Property 2
  • Caso 2: No está bien-Propiedad 3
  • Caso 3: OK-Propiedad 3
  • Caso 4: Sin confusión - Llamadas char * versión, Propiedad 2 & 3

Así la introducción de nullptr evita todos los problemas del viejo NULL.

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

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


Estándar Referencias:

C++11 Estándar: C. 3.2.4 Macro NULL
C++11: 18.2 Tipos
C++11: 4.10 Puntero conversiones
Estándar C99: 6.3.2.3 Punteros

 80
Author: Alok Save,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-17 17:19:55

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

Considere:

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 hacerlo. Las funciones de reenvío son esenciales, y 0 no puede lidiar con 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 introducir nullptr antes de lo previsto después de implementar rvalue referencias y luego descubrieron este escollo por sí mismos.

Hay algunos otros casos en los que nullptr puede hacer la vida más fácil, pero no es un caso central, ya que un elenco puede resolver estos problemas. Considere

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); }
 23
Author: Puppy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-12-11 13:48:45

Fundamentos 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 de 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 de reserva. La política principal de C++es que 0 es un int, no un puntero.

Ventaja 1-Elimine la ambigüedad al sobrecargar los tipos de puntero e integral

En C++98, la implicación principal de esto era que sobrecargar los tipos de puntero e integrales podría llevar a sorpresas. Pasar 0 o NULL a tales sobrecargas nunca se llama 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 a fun con NULL-el puntero nulo") y su significado real ("Estoy llamando a la diversión con algún tipo de entero - no el puntero nulo").

La ventaja de Nullptr es que no tiene un tipo integral. Llamar a la función sobrecargada fun con nullptr llama a void * overload (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 sorpresas de resolución de sobrecarga.

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

Por ejemplo, supongamos que encuentra esto en un código base:

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

Si no sabe (o no puede averiguar fácilmente) qué devuelve findRecord, puede no estar claro si result es un tipo puntero o un tipo integral. Después de todo, 0 (contra qué resultado se prueba) podría ir de cualquier manera. Si usted ve lo siguiente, por otro lado,

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

No hay ambigüedad: el resultado debe ser de tipo 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 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 plantilla para todos estos lockAndCallF1, lockAndCallF2 & lockAndCallF3. Así que se puede generalizar con plantilla. He escrito template function lockAndCall en lugar de multiple definition lockAndCallF1, lockAndCallF2 & lockAndCallF3 para código redundante.

El código se re-factoriza 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 por qué falló la compilación para lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr) no para lockAndCall(f3, f3m, nullptr)

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

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

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

En contraste, la llamada que involucra nullptr no tiene problemas. Cuando nullptr se pasa a lockAndCall, el tipo para ptr se deduce que 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, utilizar nullptr, no 0 o NULL.

 5
Author: Ajay yadav,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-17 17:12:30

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

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

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

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

nullptr hace más , fácil e intuitivo:

foo(nullptr);

Enlace adicional de 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)
 4
Author: iammilind,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-12-11 08:46:39

Al igual que otros ya han dicho, su principal ventaja radica en las sobrecargas. Y aunque las sobrecargas explícitas int vs. puntero 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 compila: Cannot convert int to MyClass*.

 4
Author: Angew,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-12-11 08:52:33

IMO más importante que esos problemas de sobrecarga: en construcciones de plantillas profundamente anidadas, es difícil no perder la pista de los tipos, y dar firmas explícitas es todo un esfuerzo. Por lo tanto, para todo lo que use, cuanto más preciso esté enfocado al propósito previsto, mejor, reducirá la necesidad de firmas explícitas y permitirá que el compilador produzca mensajes de error más detallados cuando algo sale mal.

 2
Author: leftaroundabout,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-06-22 22:17:55