c++ - roben - ¿Cuáles son las ventajas de usar nullptr?
programa para proteger mi wifi de intrusos (7)
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 compila: Cannot convert int to MyClass*
.
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
?
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 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 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 mientras que de void*
a cualquier tipo mientras que C ++ exige un reparto explícito). Esto hace que la definición de NULL especificada por 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. Ambas expresiones anteriores no funcionarían.
- Caso 1: No se compilará porque se necesita un lanzamiento automático de
void *
astd::string
. - Caso 2: No se compilará 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<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 como el resultado muestra la función que toma una versión int se llama. Esto es porque NULL es un literal numérico.
Además, dado que se define la implementación de si NULL puede ser 0 o 0L, puede haber mucha confusión en la resolución de sobrecarga de la función.
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 de arriba:
- Caso 1: llamadas
doSomething(char *)
como se esperaba - Caso 2: llama a
doSomething(int)
pero quizás se desee la versiónchar*
porque0
IS también es un puntero nulo. - Caso 3: si
NULL
se define como0
,
llama adoSomething(int)
cuando quizás se pretendía hacer algodoSomething(char *)
, lo que tal vez provocara errores de lógica en el tiempo de ejecución.
Mientras, siNULL
se define como0L
,
La llamada es ambigua y da como resultado 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 ++ 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 en 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<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 - 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
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<typename T, T *ptr>
struct something{}; //primary template
template<>
struct something<nullptr_t, nullptr>{}; //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<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 a f(int)
.
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 objetivo, mejor reducirá la necesidad de firmas explícitas y permitirá al compilador emitir mensajes de error más perspicaces cuando algo vaya mal.
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 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); }
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)
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<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 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<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 para lockAndCall(f3, f3m, nullptr)
¿Por qué lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)
compilació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<int>
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<int>
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<int>
.
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<int>
.
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
.