web-dev-qa-db-fra.com

Quels sont les avantages d'utiliser nullptr?

Ce morceau de code conceptuellement fait 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 les pointeurs nullptr à ceux-ci les valeurs NULL ou 0?

156
Mark Garcia

Dans ce code, il ne semble pas y avoir d’avantage. Mais considérons 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 entendu, l’intention ici est d’appeler f(char const *), mais en réalité, f(int) sera appelée! C'est un gros problème1n'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. Voici un autre:

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

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

Depuis dans template, le type de nullptr est 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++, NULL est défini comme #define NULL 0, Il s'agit donc essentiellement de int, c'est pourquoi la fonction f(int) est appelée.

173
Nawaz

C++ 11 introduit nullptr, il est connu comme constante du pointeur Null et il améliore la sécurité du type et résout les situations ambiguës contrairement à la constante du pointeur null dépendant de la mise en œuvre existante NULL. Pour pouvoir 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?

Pre C++ 11 NULL a été utilisé pour représenter un pointeur qui n'a pas de valeur ou un pointeur qui ne pointe pas sur une valeur valide. Contrairement à la notion 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. Considérez le 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'implémentation définie définie dans certains fichiers d'en-tête de bibliothèque standard. L'origine de NULL provient de C et C++ l'a héritée de C. Le standard C a défini 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é (le C ne nécessite pas de conversion explicite de void* Vers un type quelconque, alors que C++ impose une conversion explicite). Cela rend la définition de NULL spécifiée par le standard 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 compilera pas car une conversion automatique est requise de void * À std::string.
  • Cas 2: Ne compilera pas car la conversion de void * En pointeur sur une fonction membre est nécessaire.

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


Alors, quelle est la nécessité d’une autre constante de pointeur null lorsque nous avons déjà NULL?

Bien que le comité de normalisation C++ ait proposé une définition NULL qui fonctionne pour C++, cette définition a son lot de problèmes. NULL a assez bien fonctionné pour presque tous les scénarios mais pas tous. Cela 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

De toute évidence, l'intention semble être d'appeler la version qui prend comme argument l'argument char*, Mais comme la sortie le montre, la fonction qui prend un int version est appelée. En effet, NULL est un littéral numérique.

De plus, comme il est défini par l'implémentation que NULL soit égal à 0 ou 0L, il peut y avoir beaucoup de confusion dans la résolution de la surcharge des fonctions.

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: appelle doSomething(int), mais peut-être que la version char* Était souhaitée car 0 IS est également un pointeur nul.
  • Cas 3: Si NULL est défini comme 0, Appelle doSomething(int) lorsque peut-être doSomething(char *) était destiné, entraînant peut-être erreur de logique lors de l'exécution. Si NULL est 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 différents résultats, ce qui est clairement indésirable. Naturellement, le comité de normalisation C++ a voulu corriger ceci et c’est la principale motivation de nullptr.


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

C++ 11 introduit un nouveau mot clé nullptr pour servir de constante de pointeur NULL. Contrairement à NULL, son comportement n'est pas défini par l'implémentation. Ce n'est pas une macro mais il 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
Property 2: il est implicitement convertible et comparable à tout type de pointeur ou de type pointeur à membre, mais
Property 3: il n'est pas implicitement convertible ou comparable aux types intégraux, à l'exception de 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 - Appelle la version char *, Propriétés 2 et 3

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

Comment et où devriez-vous 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é.


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
Norme C99: 6.3.2.3 Pointeurs

84
Alok Save

La vraie motivation ici est 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 un spécial valeur, mais les valeurs ne peuvent pas se propager à travers les types uniquement système. Les fonctions de transfert sont essentielles et 0 ne peuvent pas les gérer. Ainsi, il était absolument nécessaire d’introduire nullptr, où type 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 avoir implémenté les références rvalue, puis découvert ce piège.

Il existe quelques autres cas où nullptr peut rendre la vie plus facile - mais ce n'est pas un cas fondamental, 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 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); }
23
Puppy

bases de nullptr

std::nullptr_t Est le type du littéral de pointeur null, nullptr. C'est une prvalue/rvalue de type std::nullptr_t. Il existe des conversions implicites de nullptr vers la valeur null du pointeur 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 il s’agit d’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 d’une surcharge des types pointeur et intégrale

En C++ 98, la principale implication de cela était que la surcharge des types pointeur et intégral pouvait être source de 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*)

Ce qui est intéressant à propos de cet appel, c’est la contradiction entre le sens apparent du code source ("J’appelle fun avec NULL, le pointeur nul") et son sens réel ("J’appelle fun avec un type d’entier, et non le null aiguille").

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 être considéré comme faisant partie intégrante:

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

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

n autre avantage de nullptr par rapport à NULL(0) lors de l'utilisation de 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 trouver facilement) ce que findRecord renvoie, il peut ne pas être clair si le résultat est un type de pointeur ou un type intégral. Après tout, 0 (contre quel résultat est testé) pourrait aller dans les deux sens. Si vous voyez ce qui suit, par contre,

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

il n’ya aucune ambiguïté: le résultat doit être un type de pointeur.

Avantage

#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 & lockAndCallF3 ont un code redondant. Il est dommage d'écrire un 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 un modèle. J'ai écrit la fonction template lockAndCall à la place 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;
}

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, donc c'est le type du paramètre ptr dans l'instanciation de cet appel à lockAndCall. Malheureusement, cela signifie que dans l’appel à func inside lockAndCall, un int est transmis et que cela n’est pas compatible avec le paramètre std::shared_ptr<int> Que f1 Attend. Le 0 passé dans l'appel à lockAndCall était censé représenter un pointeur nul, mais ce qui a été passé est en réalité int. Essayer de transmettre 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 entier est transmis à une fonction qui nécessite un std::shared_ptr<int>.

L'analyse pour l'appel impliquant NULL est essentiellement la même. Lorsque NULL est passé à lockAndCall, un type entier est déduit pour le paramètre ptr et une erreur de type se produit lorsque ptr (type int ou de type int) est transmis à 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, le type de ptr est déduit comme étant std::nullptr_t. Lorsque ptr est passé à f3, Il y a une conversion implicite de std::nullptr_t À int*, Car std::nullptr_t Est convertie implicitement en tous les types de pointeur.

Il est recommandé, lorsque vous souhaitez faire référence à un pointeur null, utilisez nullptr, pas 0 ou NULL.

5
Ajay yadav

Comme d'autres l'ont déjà dit, son principal avantage réside dans les surcharges. Et bien que les surcharges de pointeur explicites int et de pointeurs puissent être rares, considérez les fonctions de bibliothèque standard telles que 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
Angew

Il n'y a pas d'avantage direct à avoir nullptr dans la manière dont vous avez montré les exemples.
Mais considérons une situation dans laquelle vous avez 2 fonctions portant 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, la procédure est la suivante:

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

nullptr le rend plus simple et intuitif :

foo(nullptr);

Lien supplémentaire de la page Web de Bjarne.
Peu importe, mais en note C++ 11:

auto p = 0; // makes auto as int
auto p = nullptr; // makes auto as decltype(nullptr)
4
iammilind

L'OMI 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 une véritable entreprise. Donc, pour tout ce que vous utilisez, mieux ciblés, mieux 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.

2
leftaroundabout