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
?
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é.
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 versionchar*
était souhaitée car0
est également un pointeur nul. -
cas 3: Si {[9] } est défini comme
0
, appelledoSomething(int)
Quand peut-êtredoSomething(char *)
était destiné, entraînant peut-être une erreur logique à l'exécution. SiNULL
est défini comme0L
, 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
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); }
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
.
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)
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*
.
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.