Quels sont les avantages de l'utilisation de nullptr?

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

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

Et alors, quels sont les avantages d'attribuer des pointeurs nullptr sur leur attribuer les valeurs NULL ou 0?

149
demandé sur Visruth 2012-12-11 12:35:11

7 réponses

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é {[5] } sera appelé! C'est un gros problème1, n'est-ce pas?

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

f(nullptr); //first function is called

Bien sûr, ce n'est pas le seul avantage de nullptr. Voici un autre:

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

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

Depuis le modèle, le le type de nullptr est déduit comme nullptr_t, donc vous pouvez é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++, NULL est défini comme #define NULL 0, de sorte qu'il est fondamentalement int, c'est pourquoi f(int) est appelé.

163
répondu Nawaz 2017-05-29 09:15:52

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


Qu'est-Ce que NULL exactement?

Avant C++11 NULL a été utilisé pour représenter un pointeur qui n'a pas de valeur ou pointeur qui ne pointe vers rien de VALIDE. Contrairement à l'idée populaire NULL n'est pas un mot-clé en C++. C'est un identifiant défini dans les en-têtes de bibliothèque standard. En bref, vous ne pouvez pas utiliser NULL sans inclure certains en-têtes de bibliothèque standard. Envisager l'Exemple de programme:

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

Sortie:

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 certaines normes bibliothèque fichiers d'en-tête. L'origine de NULL provient de C et C++ l'a hérité de C. La norme C définit NULL comme 0 ou (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 C standard 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 a été défini comme (void *)0, aucune des expressions ci-dessus ne fonctionnerait.

  • Cas 1: ne compilera pas car une distribution automatique est nécessaire de void * à std::string.
  • cas 2: ne compilera pas car la conversion de void * vers le pointeur vers la fonction membre est nécessaire.

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


Alors, quel est le besoin d'une autre constante de pointeur null quand on a déjà NULL?

Bien que le Comité des normes C++ ait trouvé une définition nulle qui fonctionnera pour C++, cette définition avait sa propre part de problèmes. NULL a assez bien fonctionné 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;
}

Sortie:

In Int version

Clairement, l'intention semble être d'appeler la version qui prend en char* comme l'argument, mais comme la sortie montre la fonction qui prend une version int est appelée. C'est parce que NULL est un littéral numérique.

En outre, 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 ci-dessus:

  • Cas 1: appelle doSomething(char *) comme prévu.
  • cas 2: appels doSomething(int) mais peut-être que la version char* était souhaitée car 0 est également un pointeur nul.
  • cas 3: Si {[9] } est défini comme 0, appelle doSomething(int) Quand peut-être doSomething(char *) était destiné, entraînant peut-être une erreur logique à l'exécution. Si NULL est défini comme 0L, l'appel est ambigu et entraîne une erreur de compilation.

Ainsi, selon 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++ je voulais corriger cela et c'est la principale motivation pour nullptr.


Donc, qu'est-ce que nullptr et comment faut-il éviter les problèmes de NULL?

C++11 introduit un nouveau mot clé nullptr pour servir de pointeur null constante. 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 pour éviter les inconvénients de NULL. Pour résumer sa propriétés:

Propriété 1:, il a son propre type std::nullptr_t, et
Propriété 2:, il est implicitement convertible et comparable à n'importe quel type de pointeur ou un pointeur de type de membre, mais
Propriété 3:, il n'est pas implicitement convertible ou comparable à celle des types intégraux, sauf pour les 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: Not Ok-Propriété 3
  • cas 3: OK-propriété 3
  • cas 4: Pas de confusion - appelle char * version, propriété 2 & 3

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

Comment et Où utiliser nullptr?

La règle de base pour C++11 est simplement de commencer à utiliser nullptr chaque fois que vous auriez autrement utilisé NULL dans le passé.


norme Références:

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

80
répondu Alok Save 2018-07-17 17:19:55

La vraie motivation ici est transfert parfait.

Considérons:

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

Autrement dit, 0 est une valeur spéciale , mais les valeurs ne peuvent pas se propager à travers les types système seuls. Les fonctions de transfert sont essentielles, et 0 ne peut pas les gérer. Ainsi, il était absolument nécessaire d'introduire nullptr, où le type {[9] } est ce qui est spécial, et le type peut en effet se propager. En fait, L'équipe MSVC a dû introduire nullptr plus tôt que prévu après leur mise en œuvre rvalue références et ensuite découvert cet écueil pour eux-mêmes.

Il y a quelques autres cas de coin où nullptr peut 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. En outre, 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); }
23
répondu Puppy 2012-12-11 13:48:45

Bases de nullptr

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

Le littéral 0 est un int, pas un pointeur. Si C++ se trouve à 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-Supprimer l'ambiguïté lors de la surcharge sur le pointeur et les types intégraux

En C++98, l'implication principale de ceci était que la surcharge sur le pointeur et les types intégraux pourrait 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 null") et son signification réelle ("j'appelle fun avec une sorte d'entier-pas le pointeur null").

L'avantage de Nullptr est qu'il n'a pas de type intégral. Appeler 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 

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

Un Autre avantage de nullptr sur NULL(0) lors de l'utilisation de l'auto 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 facilement savoir) ce que findRecord renvoie, il peut ne pas être clair si result est un type de pointeur ou un type intégral. Après tout, 0 (quel résultat est testé) pourrait aller de toute façon. Si vous voyez ce qui suit, d'autre part,

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

Il n'y a pas d'ambiguïté: le résultat doit être un type de 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 compile et s'exécute avec succès mais lockAndCallF1, lockAndCallF2 et lockAndCallF3 ont un code redondant. Il est dommage d'écrire du code comme celui-ci si nous pouvons écrire un modèle pour tous ces lockAndCallF1, lockAndCallF2 & lockAndCallF3. Donc, il peut être généralisé avec le modèle. J'ai écrit la fonction de modèle lockAndCall au lieu de la définition multiple lockAndCallF1, lockAndCallF2 & lockAndCallF3 pour 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;
}

Une analyse Détaillée pourquoi la compilation a échoué pour lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr) pas pour lockAndCall(f3, f3m, nullptr)

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

Le problème est que lorsque 0 est passé à lockAndCall, la déduction de type de modèle entre en jeu pour déterminer son type. Le type de 0 est int, c'est donc le type du paramètre ptr dans 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 paramètre std::shared_ptr<int> que f1 attend. Le 0 passé dans le l'appel à lockAndCall était destiné à représenter un pointeur nul, mais ce qui a été réellement passé était int. Essayer de passer cet int à f1 en tant que std::shared_ptr<int> est une erreur de type. L'appel à lockAndCall avec 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 NULL est essentiellement la même. Lorsque NULL est passé à lockAndCall, un type intégral est déduit pour le paramètre ptr, et un type d'erreur se produit lorsque ptr-un int ou int-comme type est passé à f2, qui s'attend à obtenir un std::unique_ptr<int>.

En revanche, l'appel impliquant nullptr n'a aucun problème. Lorsque nullptr est passé à lockAndCall, les ptr est déduit à std::nullptr_t. Lorsque ptr est passé à f3, Il y a une conversion implicite de std::nullptr_t à int*, car std::nullptr_t convertit implicitement en tous les types de pointeurs.

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

5
répondu Ajay yadav 2018-07-17 17:12:30

Il n'y a aucun avantage direct d'avoir nullptr dans la façon dont vous avez montré les exemples.
Mais considérez une situation où vous avez 2 fonctions avec le même nom; 1 prend int et un autre int*

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)

nullptr le rend plus facile et intuitif :

foo(nullptr);

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

auto p = 0; // makes auto as int
auto p = nullptr; // makes auto as decltype(nullptr)
4
répondu iammilind 2012-12-11 08:46:39

Comme d'autres l'ont déjà dit, son principal avantage réside dans les surcharges. Et alors que les surcharges explicites int par rapport aux pointeurs peuvent ê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*.

4
répondu Angew 2012-12-11 08:52:33

IMO 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 à fait un effort. Donc, pour tout ce que vous utilisez, plus précisément axé sur le but prévu, mieux c'est, cela réduira le besoin de signatures explicites et permettra au compilateur de produire des messages d'erreur plus perspicaces quand quelque chose va mal.

2
répondu leftaroundabout 2014-06-22 22:17:55