Quels sont les avantages de l'utilisation de nullptr?

163

Ce morceau de code fait conceptuellement la même chose pour les trois pointeurs (initialisation sûre du pointeur):

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

Et donc, quels sont les avantages d'attribuer des pointeurs nullptrplutôt que de leur attribuer les valeurs NULLou 0?

Mark Garcia
la source
39
D'une part, une fonction surchargée prend intet void *ne choisira pas la intversion sur la void *version lors de l'utilisation nullptr.
chris
2
Eh bien, f(nullptr)c'est différent de f(NULL). Mais en ce qui concerne le code ci-dessus (attribution à une variable locale), les trois pointeurs sont exactement les mêmes. Le seul avantage est la lisibilité du code.
balki du
2
Je suis en faveur d'en faire une FAQ, @Prasoon. Merci!
sbi
1
NB NULL n'est historiquement pas garanti à 0, mais est comme oc C99, de la même manière qu'un octet n'était pas nécessairement long de 8 bits et vrai et faux étaient des valeurs dépendantes de l'architecture. Cette question se concentre sur nullptrla différence entre 0 etNULL
awiebe le

Réponses:

180

Dans ce code, il ne semble pas y avoir d'avantage. Mais considérez les fonctions surchargées suivantes:

void f(char const *ptr);
void f(int v);

f(NULL);  //which function will be called?

Quelle fonction sera appelée? Bien sûr, l'intention ici est d'appeler f(char const *), mais en réalité f(int)sera appelée! C'est un gros problème 1 , n'est-ce pas?

Donc, la solution à de tels problèmes est d'utiliser nullptr:

f(nullptr); //first function is called

Bien sûr, ce n'est pas le seul avantage de nullptr. En voici une autre:

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

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

Depuis dans le modèle, le type de nullptrest déduit comme nullptr_t, vous pouvez donc écrire ceci:

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 ++, NULLest défini comme #define NULL 0, donc c'est fondamentalement int, c'est pourquoi il f(int)est appelé.

Nawaz
la source
1
Comme l'a déclaré Mehrdad, ces types de surcharges sont assez rares. Y a-t-il d'autres avantages pertinents de nullptr? (Non, je ne suis pas exigeant)
Mark Garcia
2
@MarkGarcia, Cela pourrait être utile: stackoverflow.com/questions/13665349/…
chris
9
Votre note de bas de page semble inversée. NULLest requis par la norme pour avoir un type intégral, et c'est pourquoi il est généralement défini comme 0ou 0L. De plus, je ne suis pas sûr d'aimer cette nullptr_tsurcharge, car elle n'attrape que les appels avec nullptr, pas avec un pointeur nul d'un type différent, comme (void*)0. Mais je peux croire qu'il a quelques utilisations, même si tout ce qu'il fait est de vous éviter de définir votre propre type d'espace réservé à valeur unique pour signifier «aucun».
Steve Jessop
1
Un autre avantage (bien que mineur) peut être qu'il nullptra une valeur numérique bien définie, contrairement aux constantes de pointeur nul. Une constante de pointeur nul est convertie en pointeur nul de ce type (quel qu'il soit). Il est nécessaire que deux pointeurs nuls du même type se comparent de manière identique, et la conversion booléenne transforme un pointeur nul en false. Rien d'autre n'est requis. Par conséquent, il est possible pour un compilateur (idiot, mais possible) d'utiliser par exemple 0xabcdef1234ou un autre nombre pour le pointeur nul. D'autre part, nullptrest nécessaire pour convertir en zéro numérique.
Damon du
1
@DeadMG: Qu'est-ce qui est incorrect dans ma réponse? qui f(nullptr)n'appellera pas la fonction prévue? Il y avait plus d'une motivation. Beaucoup d'autres choses utiles peuvent être découvertes par les programmeurs eux-mêmes dans les années à venir. Vous ne pouvez donc pas dire qu'il n'y a qu'un seul véritable usage de nullptr.
Nawaz du
87

C ++ 11 introduit nullptr, il est connu comme la Nullconstante de pointeur et améliore la sécurité des types et résout les situations ambiguës contrairement à la constante de pointeur nul dépendante de l'implémentation existante NULL. Pour pouvoir comprendre les avantages de nullptr. nous devons d'abord comprendre ce qu'est NULLet quels sont les problèmes qui y sont associés.


Qu'est-ce que c'est NULLexactement?

Le pré C ++ 11 NULLétait utilisé pour représenter un pointeur sans valeur ou un pointeur qui ne pointe vers rien de valide. Contrairement à la notion courante, il NULLn'y a pas de mot-clé en C ++ . Il s'agit d'un identifiant défini dans les en-têtes de bibliothèque standard. En bref, vous ne pouvez pas utiliser NULLsans inclure certains en-têtes de bibliothèque standard. Considérez le programme exemple :

int main()
{ 
    int *ptr = NULL;
    return 0;
}

Production:

prog.cpp: In function 'int main()':
prog.cpp:3:16: error: 'NULL' was not declared in this scope

La norme C ++ définit NULL comme une macro définie par l'implémentation définie dans certains fichiers d'en-tête de bibliothèque standard. L'origine de NULL est de C et C ++ l'a hérité de C. Le standard C défini NULL comme 0ou (void *)0. Mais en C ++, il y a une différence subtile.

C ++ n'a pas pu accepter cette spécification telle quelle. Contrairement à C, C ++ est un langage fortement typé (C ne nécessite pas de conversion explicite de void*vers n'importe quel type, tandis que C ++ impose une conversion explicite). Cela rend la définition de NULL spécifiée par la norme C inutile dans de nombreuses expressions C ++. Par exemple:

std::string * str = NULL;         //Case 1
void (A::*ptrFunc) () = &A::doSomething;
if (ptrFunc == NULL) {}           //Case 2

Si NULL était défini comme (void *)0, aucune des expressions ci-dessus ne fonctionnerait.

  • Cas 1: ne sera pas compilé car une conversion automatique est nécessaire de void *à std::string.
  • Cas 2: ne sera pas compilé car un cast de void *vers pointeur vers une fonction membre est nécessaire.

Donc, contrairement à C, C ++ Standard mandaté pour définir NULL comme littéral numérique 0ou 0L.


Alors, quel est le besoin d'une autre constante de pointeur nul alors que nous l'avons NULLdéjà?

Bien que le comité des normes C ++ ait proposé une définition NULL qui fonctionnera pour C ++, cette définition avait sa propre part de problèmes. NULL fonctionnait assez bien pour presque tous les scénarios mais pas tous. Il a donné des résultats surprenants et erronés pour certains scénarios rares. Par exemple :

#include<iostream>
void doSomething(int)
{
    std::cout<<"In Int version";
}
void doSomething(char *)
{
   std::cout<<"In char* version";
}

int main()
{
    doSomething(NULL);
    return 0;
}

Production:

In Int version

Clairement, l'intention semble être d'appeler la version qui prend char*comme argument, mais comme la sortie le montre, la fonction qui prend une intversion est appelée. En effet, NULL est un littéral numérique.

De plus, comme il est défini par l'implémentation si NULL est 0 ou 0L, il peut y avoir beaucoup de confusion dans la résolution de surcharge de fonction.

Exemple de programme:

#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
}

Analyse de l'extrait de code ci-dessus:

  • Cas 1: appelle doSomething(char *)comme prévu.
  • Cas 2: appels doSomething(int)mais peut-être la char*version était-elle souhaitée car 0EST également un pointeur nul.
  • Cas 3: Si NULLest défini comme 0, appelle doSomething(int)quand peut-être doSomething(char *)était prévu, ce qui peut entraîner une erreur de logique lors de l'exécution. Si NULLest défini comme 0L, l'appel est ambigu et entraîne une erreur de compilation.

Ainsi, en fonction de la mise en œuvre, le même code peut donner divers résultats, ce qui est clairement indésirable. Naturellement, le comité des normes C ++ a voulu corriger cela et c'est la principale motivation de nullptr.


Alors qu'est-ce que c'est nullptret comment évite-t-il les problèmes de NULL?

C ++ 11 introduit un nouveau mot clé nullptrpour servir de constante de pointeur nul. Contrairement à NULL, son comportement n'est pas défini par l'implémentation. Ce n'est pas une macro mais elle a son propre type. nullptr a le type std::nullptr_t. C ++ 11 définit de manière appropriée les propriétés du nullptr afin d'éviter les inconvénients de NULL. Pour résumer ses propriétés:

Propriété 1: il a son propre type std::nullptr_t, et
Propriété 2: il est implicitement convertible et comparable à tout type de pointeur ou type de pointeur vers membre, mais
Propriété 3: il n'est pas implicitement convertible ou comparable à des types intégraux, sauf pour bool.

Prenons l'exemple suivant:

#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;
}

Dans le programme ci-dessus,

  • Cas 1: OK - Propriété 2
  • Cas 2: Pas OK - Propriété 3
  • Cas 3: OK - Propriété 3
  • Cas 4: Pas de confusion - char *Version des appels , propriétés 2 et 3

Ainsi l'introduction de nullptr évite tous les problèmes du bon vieux NULL.

Comment et où utiliser nullptr?

La règle d'or pour C ++ 11 est simplement de commencer à utiliser nullptrchaque fois que vous auriez autrement utilisé NULL dans le passé.


Références standard:

C ++ 11 Standard: C.3.2.4 Macro NULL
C ++ 11 Standard: 18.2 Types
C ++ 11 Standard: 4.10 Conversions de pointeur
C99 Standard: 6.3.2.3 Pointeurs

Alok Save
la source
Je pratique déjà votre dernier conseil depuis que je l'ai su nullptr, bien que je ne sache pas quelle différence cela fait vraiment à mon code. Merci pour la bonne réponse et surtout pour l'effort. M'a apporté beaucoup de lumière sur le sujet.
Mark Garcia
"dans certains fichiers d'en-tête de bibliothèque standard." -> pourquoi ne pas simplement écrire "cstddef" depuis le début?
mxmlnkn
Pourquoi devrions-nous autoriser nullptr à être convertible en type booléen? Pourriez-vous s'il vous plaît élaborer plus?
Robert Wang
... a été utilisé pour représenter un pointeur sans valeur ... Les variables ont toujours une valeur. Cela peut être du bruit ou 0xccccc...., mais, une variable sans valeur est une contradiction inhérente.
3Dave
"Cas 3: OK - Propriété 3" (ligne bool flag = nullptr;). Non, pas OK, j'obtiens l'erreur suivante au moment de la compilation avec g ++ 6:error: converting to ‘bool’ from ‘std::nullptr_t’ requires direct-initialization [-fpermissive]
Georg
23

La vraie motivation ici est une transmission parfaite .

Considérer:

void f(int* p);
template<typename T> void forward(T&& t) {
    f(std::forward<T>(t));
}
int main() {
    forward(0); // FAIL
}

En termes simples, 0 est une valeur spéciale , mais les valeurs ne peuvent pas se propager à travers le système, seuls les types le peuvent. Les fonctions de transfert sont essentielles et 0 ne peut pas les gérer. Ainsi, il fallait absolument introduire nullptr, où le type est ce qui est spécial, et le type peut effectivement se propager. En fait, l'équipe MSVC a dû se présenter plus nullptrtôt que prévu après avoir implémenté des références rvalue, puis découvert cet écueil par elle-même.

Il y a quelques autres cas de coin où nullptrpeuvent rendre la vie plus facile - mais ce n'est pas un cas de base, car un casting peut résoudre ces problèmes. Considérer

void f(int);
void f(int*);
int main() { f(0); f(nullptr); }

Appelle deux surcharges distinctes. De plus, considérez

void f(int*);
void f(long*);
int main() { f(0); }

C'est ambigu. Mais, avec nullptr, vous pouvez fournir

void f(std::nullptr_t)
int main() { f(nullptr); }
Chiot
la source
7
Drôle. La moitié de la réponse est la même que les deux autres réponses qui selon vous sont des réponses "tout à fait incorrectes" !!!
Nawaz
Le problème de transfert peut également être résolu avec un casting. forward((int*)0)travaux. Est-ce que je manque quelque chose?
jcsahnwaldt Réintègre Monica
5

Principes de base de nullptr

std::nullptr_test le type du littéral de pointeur nul, nullptr. C'est une prvalue / rvalue de type std::nullptr_t. Il existe des conversions implicites de nullptr en valeur de pointeur null de tout type de pointeur.

Le littéral 0 est un int, pas un pointeur. Si C ++ se retrouve à regarder 0 dans un contexte où seul un pointeur peut être utilisé, il interprétera à contrecœur 0 comme un pointeur nul, mais c'est une position de repli. La politique principale de C ++ est que 0 est un int, pas un pointeur.

Avantage 1 - Supprimez l'ambiguïté lors de la surcharge sur les types pointeur et intégrale

En C ++ 98, la principale implication de ceci était que la surcharge sur les types pointeur et intégrale pouvait conduire à des surprises. Passer 0 ou NULL à de telles surcharges n'a jamais appelé une surcharge de pointeur:

   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*)

La chose intéressante à propos de cet appel est la contradiction entre la signification apparente du code source («j'appelle fun avec NULL-le pointeur nul») et sa signification réelle («j'appelle fun avec une sorte d'entier - pas le null aiguille").

L'avantage de nullptr est qu'il n'a pas de type intégral. L'appel de la fonction surchargée fun avec nullptr appelle la surcharge void * (c'est-à-dire la surcharge du pointeur), car nullptr ne peut pas être considéré comme une intégrale:

fun(nullptr); // calls fun(void*) overload 

Utiliser nullptr au lieu de 0 ou NULL évite ainsi les surprises de résolution de surcharge.

Un autre avantage de nullptrsur NULL(0)lors de l'utilisation automatique pour le type de retour

Par exemple, supposons que vous rencontriez ceci dans une base de code:

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

Si vous ne savez pas (ou ne pouvez pas trouver facilement) ce que findRecord retourne, il peut ne pas être clair si result est un type pointeur ou un type intégral. Après tout, 0 (quel résultat est testé) pourrait aller dans les deux sens. Si vous voyez ce qui suit, en revanche,

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

il n'y a pas d'ambiguïté: le résultat doit être un type pointeur.

Avantage 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;
}

Le programme ci-dessus est compilé et exécuté avec succès mais lockAndCallF1, lockAndCallF2 et lockAndCallF3 ont un code redondant. Il est dommage d'écrire un code comme celui-ci si nous pouvons écrire un modèle pour tout cela lockAndCallF1, lockAndCallF2 & lockAndCallF3. Ainsi, il peut être généralisé avec un modèle. J'ai écrit une fonction de modèle lockAndCallau lieu d'une définition multiple lockAndCallF1, lockAndCallF2 & lockAndCallF3pour le code redondant.

Le code est re-factorisé comme ci-dessous:

#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;
}

Analyse détaillée pourquoi la compilation a échoué pour lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)non pourlockAndCall(f3, f3m, nullptr)

Pourquoi la compilation a lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)échoué?

Le problème est que lorsque 0 est passé à lockAndCall, la déduction du type de modèle intervient pour déterminer son type. Le type de 0 est int, c'est donc le type du paramètre ptr à l'intérieur de l'instanciation de cet appel à lockAndCall. Malheureusement, cela signifie que dans l'appel à func à l'intérieur de lockAndCall, un int est passé, et ce n'est pas compatible avec le std::shared_ptr<int>paramètre f1attendu. Le 0 passé dans l'appel à lockAndCallétait destiné à représenter un pointeur nul, mais ce qui était réellement passé était int. Essayer de passer cet int à f1 comme a std::shared_ptr<int>est une erreur de type. L'appel à lockAndCallavec 0 échoue car à l'intérieur du modèle, un int est passé à une fonction qui nécessite un std::shared_ptr<int>.

L'analyse de l'appel impliquant NULLest essentiellement la même. Quand NULLest passé à lockAndCall, un type intégral est déduit pour le paramètre ptr, et une erreur de type se produit lorsque ptr—un type int ou de type int — est passé à f2, qui s'attend à obtenir un std::unique_ptr<int>.

En revanche, l'appel impliquant nullptrn'a aucun problème. Quand nullptrest passé à lockAndCall, le type de ptrest déduit comme étant std::nullptr_t. Quand ptrest passé à f3, il y a une conversion implicite de std::nullptr_tà int*, car std::nullptr_tconvertit implicitement en tous les types de pointeurs.

Il est recommandé, chaque fois que vous voulez faire référence à un pointeur nul, utilisez nullptr, pas 0 ou NULL.

Ajay yadav
la source
4

Il n'y a aucun avantage direct à avoir nullptrde la manière dont vous avez montré les exemples.
Mais considérez une situation où vous avez 2 fonctions avec le même nom; 1 prend intet un autre unint*

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

Si vous voulez appeler foo(int*)en passant un NULL, alors le chemin est:

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

nullptrle rend plus simple et intuitif :

foo(nullptr);

Lien supplémentaire de la page Web de Bjarne.
Non pertinent mais sur la note latérale de C ++ 11:

auto p = 0; // makes auto as int
auto p = nullptr; // makes auto as decltype(nullptr)
iammilind
la source
3
Pour référence, decltype(nullptr)est std::nullptr_t.
chris
2
@MarkGarcia, c'est un type à part entière pour autant que je sache.
chris
5
@MarkGarcia, c'est une question intéressante. cppreference a: typedef decltype(nullptr) nullptr_t;. Je suppose que je peux regarder dans la norme. Ah, je l' ai
chris
2
@DeadMG: Il y avait plus d'une motivation. Beaucoup d'autres choses utiles peuvent être découvertes par les programmeurs eux-mêmes dans les années à venir. Vous ne pouvez donc pas dire qu'il n'y a qu'un seul véritable usage de nullptr.
Nawaz
2
@DeadMG: Mais vous avez dit que cette réponse est "tout à fait incorrecte" simplement parce qu'elle ne parle pas de "la vraie motivation" dont vous avez parlé dans votre réponse. Non seulement cette réponse (et la mienne aussi) a reçu un vote défavorable de votre part.
Nawaz du
4

Comme d'autres l'ont déjà dit, son principal avantage réside dans les surcharges. Et bien que les intsurcharges explicites par rapport aux pointeurs puissent être rares, considérez les fonctions de bibliothèque standard comme std::fill(qui m'a mordu plus d'une fois en C ++ 03):

MyClass *arr[4];
std::fill_n(arr, 4, NULL);

Ne compile pas: Cannot convert int to MyClass*.

Angew n'est plus fier de SO
la source
2

IMO est plus important que ces problèmes de surcharge: dans les constructions de modèles profondément imbriquées, il est difficile de ne pas perdre la trace des types, et donner des signatures explicites est tout un effort. Donc, pour tout ce que vous utilisez, le plus précisément axé sur l'objectif visé, mieux c'est, cela réduira le besoin de signatures explicites et permettra au compilateur de produire des messages d'erreur plus perspicaces en cas de problème.

à gauche
la source