Qu'est-ce que nullptr exactement?


570

Nous avons maintenant C ++ 11 avec de nombreuses nouvelles fonctionnalités. Un nouveau intéressant et déroutant (du moins pour moi) est le nouveau nullptr.

Eh bien, plus besoin de la méchante macro NULL.

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

Pourtant, je ne comprends pas comment ça nullptrmarche. Par exemple, un article de Wikipedia dit:

C ++ 11 corrige cela en introduisant un nouveau mot clé pour servir de constante de pointeur nul distinct: nullptr. Il est de type nullptr_t , qui est implicitement convertible et comparable à tout type de pointeur ou de type pointeur sur membre. Il n'est pas implicitement convertible ou comparable aux types intégraux, à l'exception de bool.

Comment est-ce un mot-clé et une instance d'un type?

De plus, avez-vous un autre exemple (à côté de celui de Wikipedia) où nullptrest supérieur au bon vieux 0?


23
fait connexe: nullptrest également utilisé pour représenter une référence nulle pour les descripteurs gérés en C ++ / CLI.
Mehrdad Afshari

3
Lorsque vous utilisez Visual C ++, n'oubliez pas que si vous utilisez nullptr avec du code C / C ++ natif, puis compilez avec l'option de compilateur / clr, le compilateur ne peut pas déterminer si nullptr indique une valeur de pointeur null natif ou géré. Pour que votre intention soit claire pour le compilateur, utilisez nullptr pour spécifier une valeur gérée ou __nullptr pour spécifier une valeur native. Microsoft a implémenté cela comme une extension de composant.
cseder

6
Est nullptr_tgaranti d'avoir un seul membre nullptr,? Donc, si une fonction est retournée nullptr_t, le compilateur sait déjà quelle valeur sera renvoyée, quel que soit le corps de la fonction?
Aaron McDaid

8
@AaronMcDaid std::nullptr_tpeut être instancié, mais toutes les instances seront identiques à nullptrcar le type est défini comme typedef decltype(nullptr) nullptr_t. Je crois que la principale raison pour laquelle le type existe est pour que les fonctions puissent être surchargées spécifiquement pour intercepter nullptr, si nécessaire. Voir ici pour un exemple.
Justin Time - Rétablir Monica le

5
0 n'a jamais été un pointeur nul, le pointeur nul est un pointeur qui peut être obtenu en transtypant zéro littéral en type pointeur, et il ne pointe vers aucun objet existant par définition.
Swift - Friday Pie

Réponses:


403

Comment est-ce un mot-clé et une instance d'un type?

Ce n'est pas surprenant. Les deux trueet falsesont des mots clés et en tant que littéraux, ils ont un type ( bool). nullptrest un pointeur littéral de type std::nullptr_t, et c'est une valeur (vous ne pouvez pas en prendre l'adresse en utilisant &).

  • 4.10À propos de la conversion de pointeur, une valeur de type std::nullptr_test une constante de pointeur nulle et une constante de pointeur nul intégrale peut être convertie en std::nullptr_t. La direction opposée n'est pas autorisée. Cela permet de surcharger une fonction pour les pointeurs et les entiers, et de passer nullptrpour sélectionner la version du pointeur. Passer NULLou 0sélectionnerait la intversion avec confusion .

  • Une conversion de nullptr_tvers un type intégral a besoin de a reinterpret_castet a la même sémantique qu'une conversion de (void*)0vers un type intégral (mise en œuvre de mappage définie). A reinterpret_castne peut pas être converti nullptr_ten n'importe quel type de pointeur. Comptez sur la conversion implicite si possible ou utilisez static_cast.

  • La norme exige que sizeof(nullptr_t)soit sizeof(void*).


Oh, après avoir regardé, il me semble que l'opérateur conditionnel ne peut pas convertir 0 en nullptr dans des cas comme cond ? nullptr : 0;. Retiré de ma réponse.
Johannes Schaub - litb

88
Notez que ce NULLn'est même pas garanti d'être 0. Il peut l'être 0L, auquel cas un appel à void f(int); void f(char *);sera ambigu. nullptrfavorisera toujours la version pointeur et n'appellera jamais celle- intci. Notez également qu'il nullptr est convertible en bool(le projet dit que à 4.12).
Johannes Schaub - litb

@litb: donc pour f (int) et f (void *) - f (0) sera-t-il toujours ambigu?
Steve Folly

27
@Steve, non qui appellera la intversion. Mais f(0L)est ambigu, car long -> intaussi bien que les long -> void*deux sont également coûteux. Donc, si NULL est 0Lsur votre compilateur, un appel f(NULL)sera ambigu étant donné ces deux fonctions. Pas si nullptrbien sûr.
Johannes Schaub - litb

2
@SvenS Il ne doit pas être défini comme (void*)0en C ++. Mais il peut être défini comme n'importe quelle constante de pointeur nul arbitraire, que toute constante intégrale de valeur 0 et nullptrremplit. Donc, certainement pas , mais le pouvez . (Vous avez oublié de me cingler btw ..)
Déduplicateur

60

De nullptr: Un pointeur nul sûr et clair :

Le nouveau mot clé nullptr C ++ 09 désigne une constante rvalue qui sert de littéral de pointeur nul universel, remplaçant le littéral buggé et faiblement typé 0 et la fameuse macro NULL. nullptr met ainsi un terme à plus de 30 ans d'embarras, d'ambiguïté et de bugs. Les sections suivantes présentent la fonction nullptr et montrent comment elle peut remédier aux problèmes de NULL et de 0.

Autres références:


17
C ++ 09? N'était-il pas appelé C ++ 0x avant août 2011?
Michael Dorst

2
@anthropomorphic Eh bien, c'est son but. C ++ 0x a été utilisé alors qu'il était encore en cours de traitement, car on ne savait pas s'il serait terminé en 2008 ou 2009. Notez qu'il est en fait devenu C ++ 0B, ​​ce qui signifie C ++ 11. Voir stroustrup.com/C++11FAQ.html
mxmlnkn le

45

Pourquoi nullptr en C ++ 11? Qu'Est-ce que c'est? Pourquoi NULL n'est-il pas suffisant?

L'expert C ++ Alex Allain le dit parfaitement ici (je souligne en gras):

... imaginez que vous avez les deux déclarations de fonction suivantes:

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

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

Bien qu'il semble que la deuxième fonction sera appelée - vous passez, après tout, ce qui semble être un pointeur - c'est vraiment la première fonction qui sera appelée! Le problème est que parce que NULL est 0 et 0 est un entier, la première version de func sera appelée à la place. C'est le genre de chose qui, oui, n'arrive pas tout le temps, mais quand cela arrive, c'est extrêmement frustrant et déroutant. Si vous ne connaissiez pas les détails de ce qui se passe, cela pourrait bien ressembler à un bogue du compilateur. Une fonctionnalité de langage qui ressemble à un bogue du compilateur n'est, eh bien, pas quelque chose que vous voulez.

Entrez nullptr. En C ++ 11, nullptr est un nouveau mot-clé qui peut (et devrait!) Être utilisé pour représenter des pointeurs NULL; en d'autres termes, où que vous écriviez NULL auparavant, vous devriez utiliser nullptr à la place. Ce n'est pas plus clair pour vous, le programmeur , (tout le monde sait ce que signifie NULL), mais c'est plus explicite pour le compilateur , qui ne verra plus les 0 partout utilisés pour avoir une signification spéciale lorsqu'ils sont utilisés comme pointeur.

Allain termine son article par:

Indépendamment de tout cela - la règle de base pour C ++ 11 est simplement de commencer à utiliser nullptrchaque fois que vous auriez autrement utilisé NULLdans le passé.

(Mes mots):

Enfin, n'oubliez pas que nullptrc'est un objet - une classe. Il peut être utilisé n'importe où a NULLété utilisé auparavant, mais si vous avez besoin de son type pour une raison quelconque, son type peut être extrait avec decltype(nullptr), ou directement décrit comme std::nullptr_t, qui est simplement un typedefde decltype(nullptr).

Références:

  1. Cprogramming.com: Meilleurs types en C ++ 11 - nullptr, classes enum (énumérations fortement typées) et cstdint
  2. https://en.cppreference.com/w/cpp/language/decltype
  3. https://en.cppreference.com/w/cpp/types/nullptr_t

2
Je dois dire que votre réponse est sous-estimée, elle était très facile à comprendre à travers votre exemple.
mss

37

Lorsque vous avez une fonction qui peut recevoir des pointeurs vers plusieurs types, l'appeler avec NULLest ambigu. La façon dont cela fonctionne maintenant est très hacky en acceptant un int et en supposant que c'est 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++11vous seriez en mesure de surcharge de nullptr_tsorte que ptr<T> p(42);serait une erreur de compilation plutôt qu'une exécution assert.

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

Et si NULLest défini comme 0L?
LF

9

nullptrne peut pas être affecté à un type intégral tel qu'un intmais uniquement un type pointeur; soit un type de pointeur intégré tel que int *ptrou un pointeur intelligent tel questd::shared_ptr<T>

Je pense que c'est une distinction importante car elle NULLpeut toujours être affectée à la fois à un type intégral et à un pointeur, tout comme NULLune macro étendue 0qui peut servir à la fois de valeur initiale pour intun pointeur et de pointeur.


Notez que cette réponse est fausse. NULLn'est pas garanti d'être étendu à 0.
LF

6

De plus, avez-vous un autre exemple (à côté de celui de Wikipedia) où nullptrest supérieur au bon vieux 0?

Oui. C'est également un exemple (simplifié) du monde réel qui s'est produit dans notre code de production. Cela ne s'est démarqué que parce que gcc a pu émettre un avertissement lors de la compilation croisée vers une plate-forme avec une largeur de registre différente (toujours pas sûr exactement pourquoi uniquement lors de la compilation croisée de x86_64 à x86, avertit warning: converting to non-pointer type 'int' from NULL):

Considérez ce code (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;
}

Il donne cette sortie:

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

Je ne vois pas comment cela s'améliore lors de l'utilisation de nullptr (et C ++ 11). Si vous définissez pb sur nullptr, la première comparaison est toujours vraie (tout en comparant les pommes avec les poires ..). Le deuxième cas est encore pire: si vous comparez a à nullptr, il convertira a en B *, puis il redeviendra vrai (avant d'être converti en bool et d'expr évalué en faux). Le tout me rappelle JavaScript et je me demande si nous aurons === en C ++ à l'avenir :(
Nils

5

Eh bien, d'autres langues ont des mots réservés qui sont des instances de types. Python, par exemple:

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

Il s'agit en fait d'une comparaison assez étroite, car elle Noneest généralement utilisée pour quelque chose qui n'a pas été initialisé, mais en même temps, des comparaisons telles queNone == 0 fausses.

En revanche, en clair C, NULL == 0retournerait vrai IIRC car NULLc'est juste une macro retournant 0, qui est toujours une adresse invalide (AFAIK).


4
NULLest une macro qui se développe à un zéro, un zéro constant à un pointeur produit un pointeur nul. Un pointeur nul ne doit pas nécessairement être nul (mais souvent), zéro n'est pas toujours une adresse non valide, et un cast nul non constant en un pointeur n'a pas besoin d'être null, et un pointeur null cast en un entier ne doit pas être nul. J'espère avoir bien compris sans rien oublier. Une référence: c-faq.com/null/null2.html
Samuel Edwin Ward

3

C'est un mot-clé car la norme le précisera comme tel. ;-) Selon le dernier projet public (n2914)

2.14.7 Littéraux de pointeur [lex.nullptr]

pointer-literal:
nullptr

Le pointeur littéral est le mot-clé nullptr. C'est une valeur de type std::nullptr_t.

C'est utile car il ne se convertit pas implicitement en une valeur intégrale.


2

Disons que vous avez une fonction (f) qui est surchargée pour prendre à la fois int et char *. Avant C ++ 11, si vous vouliez l'appeler avec un pointeur nul et que vous utilisiez NULL (c'est-à-dire la valeur 0), alors vous appelleriez celui surchargé pour int:

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

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

Ce n'est probablement pas ce que vous vouliez. C ++ 11 résout ce problème avec nullptr; Vous pouvez maintenant écrire ce qui suit:

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

1

Permettez-moi d'abord de vous donner une mise en œuvre de peu sophistiquée 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;

nullptrest un exemple subtil de l' idiome du résolveur de type de retour pour déduire automatiquement un pointeur nul du type correct en fonction du type de l'instance à laquelle il est affecté.

int *ptr = nullptr;                // OK
void (C::*method_ptr)() = nullptr; // OK
  • Comme vous pouvez le voir ci-dessus, lors de l' nullptraffectation à un pointeur entier, une intinstanciation de type de la fonction de conversion modélisée est créée. Et il en va de même pour les pointeurs de méthode.
  • De cette façon, en tirant parti de la fonctionnalité de modèle, nous créons en fait le type approprié de pointeur nul à chaque fois, une nouvelle affectation de type.
  • Comme nullptrc'est un littéral entier avec la valeur zéro, vous ne pouvez pas utiliser son adresse que nous avons accomplie en supprimant & operator.

Pourquoi avons-nous besoin nullptren premier lieu?

  • Vous voyez traditionnel NULLa un problème avec comme ci-dessous:

1️⃣ Conversion implicite

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

2️⃣ Ambiguïté d'appel de fonction

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

func(NULL);     // Which one to call?
  • La compilation produit l'erreur suivante:
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️⃣ Surcharge constructeur

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

String s1( NULL );
String s2( 5 );
  • Dans ce cas, vous avez besoin transtypage explicite (c.  -à- String s((char*)0)).

0

0 était la seule valeur entière pouvant être utilisée comme initialiseur sans transtypage pour les pointeurs: vous ne pouvez pas initialiser des pointeurs avec d'autres valeurs entières sans transtypage. Vous pouvez considérer 0 comme un singleton consexpr syntaxiquement similaire à un littéral entier. Il peut initier n'importe quel pointeur ou entier. Mais étonnamment, vous constaterez qu'il n'a pas de type distinct: il a été proposé d'être une véritable représentation constexpr singleton de valeur nulle pour initialiser les pointeurs. Il ne peut pas être utilisé pour initialiser directement des nombres entiers et élimine les ambiguïtés impliquées dans la définition en termes de 0. pourrait être défini comme une bibliothèque utilisant la syntaxe std mais semblait sémantiquement être un composant de base manquant. est désormais déconseillé en faveur de , sauf si une bibliothèque décide de le définir comme .int . Alors, comment se fait-il que 0 puisse initialiser des pointeurs et 1 non? Une réponse pratique était que nous avions besoin d'un moyen de définir la valeur nulle du pointeur et que la conversion implicite directe d' intun pointeur est sujette aux erreurs. Ainsi, 0 est devenu une véritable bête bizarre bizarre de l'ère préhistorique. nullptrNULLnullptrNULLnullptrnullptr


-1

Voici l'en-tête 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

(beaucoup peut être découvert avec un rapide grep -r /usr/include/*`)

Une chose qui saute aux yeux est la *surcharge de l'opérateur (retourner 0 est beaucoup plus convivial que segfaulting ...). Une autre chose est qu'il ne semble pas compatible avec le stockage d' une adresse à tous . Ce qui, comparé à la façon dont cela se fait en élinguant les vides * et en transmettant les résultats NULL aux pointeurs normaux en tant que valeurs sentinelles, réduirait évidemment le facteur "ne jamais oublier, ce pourrait être une bombe".


-2

NULL n'a pas besoin d'être 0. Tant que vous utilisez toujours NULL et jamais 0, NULL peut être n'importe quelle valeur. En supposant que vous programmez un microcontrôleur von Neuman avec une mémoire plate, qui a ses vektors d'interruption à 0. Si NULL est 0 et que quelque chose écrit sur un pointeur NULL, le microcontrôleur se bloque. Si NULL est disons 1024 et à 1024 il y a une variable réservée, l'écriture ne la plantera pas, et vous pouvez détecter les affectations de pointeur NULL à l'intérieur du programme. C'est inutile sur les PC, mais pour les sondes spatiales, les équipements militaires ou médicaux, il est important de ne pas planter.


2
Eh bien, la valeur réelle du pointeur nul en mémoire peut ne pas être nulle, mais la norme C (et C ++) oblige les compilateurs à convertir l'intégrale 0 littérale en pointeur nul.
bzim
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.