¿Qué es exactamente nullptr?


570

Ahora tenemos C ++ 11 con muchas características nuevas. Una interesante y confusa (al menos para mí) es la nueva nullptr.

Bueno, ya no hay necesidad de la desagradable macro NULL.

int* x = nullptr;
myclass* obj = nullptr;

Aún así, no entiendo cómo nullptrfunciona. Por ejemplo, el artículo de Wikipedia dice:

C ++ 11 corrige esto mediante la introducción de una nueva palabra clave para servir como una constante de puntero nulo distinguido: nullptr. Es del tipo nullptr_t , que es implícitamente convertible y comparable a cualquier tipo de puntero o tipo de puntero a miembro. No es implícitamente convertible o comparable a los tipos integrales, excepto bool.

¿Cómo es una palabra clave y una instancia de un tipo?

Además, ¿tiene otro ejemplo (además del de Wikipedia) en el que nullptres superior a los buenos viejos 0?


23
hecho relacionado: nullptrtambién se utiliza para representar referencias nulas para identificadores administrados en C ++ / CLI.
Mehrdad Afshari

3
Cuando use Visual C ++, recuerde que si usa nullptr con código C / C ++ nativo y luego compila con la opción del compilador / clr, el compilador no puede determinar si nullptr indica un valor de puntero nulo nativo o administrado. Para aclarar su intención al compilador, use nullptr para especificar un valor administrado o __nullptr para especificar un valor nativo. Microsoft ha implementado esto como una extensión de componente.
cseder

66
¿Se nullptr_tgarantiza tener un solo miembro nullptr? Entonces, si se devuelve una función nullptr_t, entonces el compilador ya sabe qué valor se devolverá, independientemente del cuerpo de la función.
Aaron McDaid

8
@AaronMcDaid std::nullptr_tse puede instanciar, pero todas las instancias serán idénticas nullptrporque el tipo se define como typedef decltype(nullptr) nullptr_t. Creo que la razón principal por la que existe el tipo es para que las funciones se puedan sobrecargar específicamente para capturar nullptr, si es necesario. Ver aquí para un ejemplo.
Justin Time - Restablece a Monica el

55
0 nunca fue un puntero nulo, puntero nulo es un puntero que puede ser consigue mediante fundición cero literal al tipo de puntero, y no apunta a ningún objeto existente por definición.
Swift - Viernes Pie

Respuestas:


403

¿Cómo es una palabra clave y una instancia de un tipo?

Esto no es sorprendente. Ambos truey falseson palabras clave y como literales tienen un tipo ( bool). nullptres un puntero literal de tipo std::nullptr_t, y es un prvalue (no puede tomar la dirección usando &).

  • 4.10acerca de la conversión de puntero dice que un valor de tipo std::nullptr_tes una constante de puntero nulo, y que se puede convertir una constante de puntero nulo integral std::nullptr_t. La dirección opuesta no está permitida. Esto permite sobrecargar una función tanto para punteros como enteros, y pasar nullptrpara seleccionar la versión del puntero. Pasando NULLo 0seleccionaría confusamente la intversión.

  • Un molde de nullptr_tun tipo integral necesita un reinterpret_cast, y tiene la misma semántica que un molde de (void*)0un tipo integral (implementación de mapeo definida). A reinterpret_castno se puede convertir nullptr_ta ningún tipo de puntero. Confíe en la conversión implícita si es posible o use static_cast.

  • El estándar requiere que sizeof(nullptr_t)sea sizeof(void*).


Oh, después de mirar, me parece que el operador condicional no puede convertir 0 a nullptr en casos como cond ? nullptr : 0;. Eliminado de mi respuesta.
Johannes Schaub - litb

88
Tenga en cuenta que NULLni siquiera se garantiza que sea 0. Puede ser 0L, en cuyo caso una llamada a void f(int); void f(char *);será ambigua. nullptrsiempre favorecerá la versión de puntero, y nunca llamará a la versión int. También tenga en cuenta que nullptr es convertible a bool(el borrador dice que en 4.12).
Johannes Schaub - litb

@litb: entonces con respecto a f (int) y f (void *) - ¿f (0) seguirá siendo ambiguo?
Steve Folly

27
@ Steve, no, eso llamará a la intversión. Pero f(0L)es ambiguo, porque long -> inttambién long -> void*es igualmente costoso. Entonces, si NULL está 0Len su compilador, entonces una llamada f(NULL)será ambigua dadas esas dos funciones. No es así, por nullptrsupuesto.
Johannes Schaub - litb

2
@SvenS No debe definirse como (void*)0en C ++. Pero se puede definir como cualquier constante de puntero nulo arbitrario, que cualquier constante integral con valor 0 y nullptrcumplir. Por lo tanto, definitivamente no lo hará, pero puede . (Olvidó hacerme ping por cierto ...)
Deduplicador

60

De nullptr: un puntero nulo de tipo seguro y claro :

La nueva palabra clave C ++ 09 nullptr designa una constante rvalue que sirve como un literal de puntero nulo universal, reemplazando el 0 literal con errores y de tipo débil y la infame macro NULL. nullptr pone fin a más de 30 años de vergüenza, ambigüedad y errores. Las siguientes secciones presentan la función nullptr y muestran cómo puede remediar las dolencias de NULL y 0.

Otras referencias:


17
C ++ 09? ¿No se conocía como C ++ 0x antes de agosto de 2011?
Michael Dorst

2
@anthropomorphic Bueno, ese es su propósito. C ++ 0x se usó mientras todavía estaba en progreso, porque no se sabía si terminaría 2008 o 2009. Tenga en cuenta que en realidad se convirtió en C ++ 0B, ​​que significa C ++ 11. Ver stroustrup.com/C++11FAQ.html
mxmlnkn

45

¿Por qué nullptr en C ++ 11? ¿Qué es? ¿Por qué NULL no es suficiente?

El experto en C ++ Alex Allain lo dice perfectamente aquí (mi énfasis se agrega en negrita):

... imagine que tiene las siguientes dos declaraciones de funciones:

void func(int n); 
void func(char *s);

func( NULL ); // guess which function gets called?

Aunque parece que se llamará a la segunda función, después de todo, está pasando lo que parece ser un puntero, ¡es realmente la primera función que se llamará! El problema es que debido a que NULL es 0 y 0 es un número entero, se llamará a la primera versión de func.Este es el tipo de cosa que, sí, no sucede todo el tiempo, pero cuando sucede, es extremadamente frustrante y confuso. Si no conocía los detalles de lo que está sucediendo, podría parecer un error del compilador. Una característica del lenguaje que parece un error del compilador no es algo que desee.

Ingrese nullptr. En C ++ 11, nullptr es una nueva palabra clave que puede (y debería!) Usarse para representar punteros NULL; en otras palabras, donde sea que estuviera escribiendo NULL antes, debería usar nullptr en su lugar. No es más claro para ti, el programador , (todo el mundo sabe lo que significa NULL), pero es más explícito para el compilador , que ya no verá ceros en todas partes para tener un significado especial cuando se usa como puntero.

Allain termina su artículo con:

Independientemente de todo esto, la regla general para C ++ 11 es simplemente comenzar a usar nullptrsiempre que de otro modo hubiera usadoNULL en el pasado.

(Mis palabras):

Por último, no olvides que nullptres un objeto, una clase. Se puede usar en cualquier lugar donde NULLse usaba antes, pero si necesita su tipo por alguna razón, su tipo se puede extraer decltype(nullptr)o describir directamente como std::nullptr_t, que es simplemente una typedefde decltype(nullptr).

Referencias

  1. Cprogramming.com: mejores tipos en C ++ 11 - nullptr, clases de enumeración (enumeraciones fuertemente tipadas) y cstdint
  2. https://en.cppreference.com/w/cpp/language/decltype
  3. https://en.cppreference.com/w/cpp/types/nullptr_t

2
Debo decir que su respuesta está subestimada, fue muy fácil de entender a través de su ejemplo.
mss

37

Cuando tiene una función que puede recibir punteros a más de un tipo, llamarlo con NULLes ambiguo. La forma en que esto se soluciona ahora es muy hacky al aceptar un int y asumir que es NULL.

template <class T>
class ptr {
    T* p_;
    public:
        ptr(T* p) : p_(p) {}

        template <class U>
        ptr(U* u) : p_(dynamic_cast<T*>(u)) { }

        // Without this ptr<T> p(NULL) would be ambiguous
        ptr(int null) : p_(NULL)  { assert(null == NULL); }
};

En C++11sería capaz de sobrecargarse, por nullptr_tlo que ptr<T> p(42);sería un error en tiempo de compilación en lugar de un tiempo de ejecución assert.

ptr(std::nullptr_t) : p_(nullptr)  {  }

¿Qué pasa si NULLse define como 0L?
LF

9

nullptrno se puede asignar a un tipo integral como, por ejemplo, un inttipo de puntero; ya sea un tipo de puntero incorporado comoint *ptr o un puntero inteligente comostd::shared_ptr<T>

Creo que esta es una distinción importante porque NULLtodavía se puede asignar tanto a un tipo integral como a un puntero, ya que NULLes una macro expandida a la 0que puede servir tanto como un valor inicial para un intpuntero como para un puntero.


Tenga en cuenta que esta respuesta es incorrecta. NULLno se garantiza que se expanda a 0.
LF

6

Además, ¿tiene otro ejemplo (además del de Wikipedia) en el que nullptres superior al buen 0 anterior?

Si. También es un ejemplo (simplificado) del mundo real que ocurrió en nuestro código de producción. Solo se destacó porque gcc pudo emitir una advertencia al realizar una compilación cruzada a una plataforma con un ancho de registro diferente (aún no está seguro exactamente por qué solo cuando se realiza una compilación cruzada de x86_64 a x86, adviertewarning: converting to non-pointer type 'int' from NULL ):

Considere este código (C ++ 03):

#include <iostream>

struct B {};

struct A
{
    operator B*() {return 0;}
    operator bool() {return true;}
};

int main()
{
    A a;
    B* pb = 0;
    typedef void* null_ptr_t;
    null_ptr_t null = 0;

    std::cout << "(a == pb): " << (a == pb) << std::endl;
    std::cout << "(a == 0): " << (a == 0) << std::endl; // no warning
    std::cout << "(a == NULL): " << (a == NULL) << std::endl; // warns sometimes
    std::cout << "(a == null): " << (a == null) << std::endl;
}

Produce esta salida:

(a == pb): 1
(a == 0): 0
(a == NULL): 0
(a == null): 1

No veo cómo mejora esto cuando uso nullptr (y C ++ 11). Si establece pb en nullptr, la primera comparación se evalúa como verdadera (al comparar manzanas con peras ...). El segundo caso es aún peor: si compara a a nullptr, convertirá a a B * y luego evaluará a verdadero nuevamente (antes de que se convirtiera en bool y el expr se evaluara a falso). Todo el asunto me recuerda a JavaScript y me pregunto si obtendremos === en C ++ en el futuro :(
Nils

5

Bueno, otros idiomas tienen palabras reservadas que son instancias de tipos. Python, por ejemplo:

>>> None = 5
  File "<stdin>", line 1
SyntaxError: assignment to None
>>> type(None)
<type 'NoneType'>

Esta es en realidad una comparación bastante estrecha porque Nonegeneralmente se usa para algo que no se ha inicializado, pero al mismo tiempo comparaciones comoNone == 0 son falsas.

Por otro lado, en C simple, NULL == 0devolvería IIRC verdadero porque NULLes solo una macro que devuelve 0, que siempre es una dirección no válida (AFAIK).


44
NULLes una macro que se expande a cero, una conversión constante de cero a un puntero produce un puntero nulo. Un puntero nulo no tiene que ser cero (pero a menudo lo es), el cero no siempre es una dirección no válida, y una conversión de cero no constante a un puntero no tiene que ser nula, y un puntero nulo emitido a un entero no tiene que ser cero. Espero haberlo hecho bien sin olvidar nada. Una referencia: c-faq.com/null/null2.html
Samuel Edwin Ward

3

Es una palabra clave porque el estándar la especificará como tal. ;-) Según el último borrador público (n2914)

2.14.7 Literales de puntero [lex.nullptr]

pointer-literal:
nullptr

El puntero literal es la palabra clave nullptr. Es un valor de tipo std::nullptr_t.

Es útil porque no se convierte implícitamente en un valor integral.


2

Digamos que tiene una función (f) que está sobrecargada para tomar tanto int como char *. Antes de C ++ 11, si deseaba llamarlo con un puntero nulo y usaba NULL (es decir, el valor 0), llamaría al sobrecargado para int:

void f(int);
void f(char*);

void g() 
{
  f(0); // Calls f(int).
  f(NULL); // Equals to f(0). Calls f(int).
}

Esto probablemente no sea lo que querías. C ++ 11 resuelve esto con nullptr; Ahora puedes escribir lo siguiente:

void g()
{
  f(nullptr); //calls f(char*)
}

1

Déjame primero darte una implementación de sofisticación nullptr_t

struct nullptr_t 
{
    void operator&() const = delete;  // Can't take address of nullptr

    template<class T>
    inline operator T*() const { return 0; }

    template<class C, class T>
    inline operator T C::*() const { return 0; }
};

nullptr_t nullptr;

nullptres un ejemplo sutil de la expresión de resolución de tipo de retorno para deducir automáticamente un puntero nulo del tipo correcto dependiendo del tipo de instancia a la que se está asignando.

int *ptr = nullptr;                // OK
void (C::*method_ptr)() = nullptr; // OK
  • Como puede ver arriba, cuando nullptrse asigna a un puntero entero, unint se crea instanciación de tipo de la función de conversión de plantilla. Y lo mismo ocurre con los punteros de método también.
  • De esta manera, al aprovechar la funcionalidad de la plantilla, en realidad estamos creando el tipo apropiado de puntero nulo cada vez que lo hacemos, una nueva asignación de tipo.
  • Como nullptres un literal entero con valor cero, no puede usar su dirección, lo que logramos al eliminar & operador.

¿Por qué necesitamos nullptren primer lugar?

  • Ves tradicional NULLtiene algún problema con él como a continuación:

1️⃣ Conversión implícita

char *str = NULL; // Implicit conversion from void * to char *
int i = NULL;     // OK, but `i` is not pointer type

2️⃣ Función que llama ambigüedad

void func(int) {}
void func(int*){}
void func(bool){}

func(NULL);     // Which one to call?
  • La compilación produce el siguiente error:
error: call to 'func' is ambiguous
    func(NULL);
    ^~~~
note: candidate function void func(bool){}
                              ^
note: candidate function void func(int*){}
                              ^
note: candidate function void func(int){}
                              ^
1 error generated.
compiler exit status 1

3️⃣ Sobrecarga del constructor

struct String
{
    String(uint32_t)    {   /* size of string */    }
    String(const char*) {       /* string */        }
};

String s1( NULL );
String s2( 5 );
  • En tales casos, es necesario conversión explícita (es decir,  String s((char*)0)).

0

0 solía ser el único valor entero que podría usarse como un inicializador sin conversión para punteros: no puede inicializar punteros con otros valores enteros sin una conversión. Puede considerar 0 como un singleton consexpr sintácticamente similar a un literal entero. Puede iniciar cualquier puntero o entero. Pero sorprendentemente, encontrará que no tiene un tipo distinto: es un int. Entonces, ¿cómo es que 0 puede inicializar punteros y 1 no? Una respuesta práctica fue que necesitamos un medio para definir el valor nulo del puntero y la conversión directa implícita de intun puntero es propenso a errores. Por lo tanto, 0 se convirtió en una verdadera bestia extraña y extraña de la era prehistórica. nullptrse propuso ser una representación constexpr real singleton de valor nulo para inicializar punteros. No se puede utilizar para inicializar directamente los enteros y elimina las ambigüedades relacionadas con la definición NULLen términos de 0. nullptrpodría definirse como una biblioteca que utiliza la sintaxis estándar pero parece que semánticamente es un componente central que falta. NULLahora está en desuso a favor de nullptr, a menos que alguna biblioteca decida definirlo como nullptr.


-1

Aquí está el encabezado LLVM.

// -*- C++ -*-
//===--------------------------- __nullptr --------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#ifndef _LIBCPP_NULLPTR
#define _LIBCPP_NULLPTR

#include <__config>

#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
#pragma GCC system_header
#endif

#ifdef _LIBCPP_HAS_NO_NULLPTR

_LIBCPP_BEGIN_NAMESPACE_STD

struct _LIBCPP_TEMPLATE_VIS nullptr_t
{
    void* __lx;

    struct __nat {int __for_bool_;};

    _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR nullptr_t() : __lx(0) {}
    _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR nullptr_t(int __nat::*) : __lx(0) {}

    _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR operator int __nat::*() const {return 0;}

    template <class _Tp>
        _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR
        operator _Tp* () const {return 0;}

    template <class _Tp, class _Up>
        _LIBCPP_INLINE_VISIBILITY
        operator _Tp _Up::* () const {return 0;}

    friend _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR bool operator==(nullptr_t, nullptr_t) {return true;}
    friend _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR bool operator!=(nullptr_t, nullptr_t) {return false;}
};

inline _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR nullptr_t __get_nullptr_t() {return nullptr_t(0);}

#define nullptr _VSTD::__get_nullptr_t()

_LIBCPP_END_NAMESPACE_STD

#else  // _LIBCPP_HAS_NO_NULLPTR

namespace std
{
    typedef decltype(nullptr) nullptr_t;
}

#endif  // _LIBCPP_HAS_NO_NULLPTR

#endif  // _LIBCPP_NULLPTR

(se puede descubrir mucho con un rápido grep -r /usr/include/*`)

Una cosa que salta a la vista es la *sobrecarga del operador (devolver 0 es mucho más amigable que segfaulting ...). Otra cosa es que no parece compatible con el almacenamiento de una dirección en absoluto . Lo cual, en comparación con la forma en que se lanza el vacío * y pasa los resultados NULL a los punteros normales como valores centinela, obviamente reduciría el factor "nunca olvidar, podría ser una bomba".


-2

NULL no necesita ser 0. Mientras use siempre NULL y nunca 0, NULL puede ser cualquier valor. Suponiendo que programe un microcontrolador von Neuman con memoria plana, que tenga sus interruptores en 0. Si NULL es 0 y algo escribe en un puntero NULL, el microcontrolador falla. Si NULL es digamos 1024 y en 1024 hay una variable reservada, la escritura no la bloqueará, y puede detectar asignaciones de puntero NULL desde dentro del programa. Esto no tiene sentido en las PC, pero para sondas espaciales, equipos militares o médicos es importante no chocar.


2
Bueno, el valor real del puntero nulo en la memoria puede no ser cero, pero el estándar C (y C ++) obliga a los compiladores a convertir el entero 0 literal en puntero nulo.
bzim
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.