¿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
?
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)
.
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 *
astd::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 vezchar*
se deseaba la versión porque0
también es un puntero nulo. -
Caso 3: Si
NULL
se define como0
, llama adoSomething(int)
cuando quizás se pretendíadoSomething(char *)
, lo que puede resultar en un error lógico en tiempo de ejecución. SiNULL
se define como0L
, 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
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); }
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
.
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)
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*
.
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.
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