Dans les langages typés dynamiquement comme JavaScript ou PHP, je fais souvent des fonctions telles que:
function getSomething(name) {
if (content_[name]) return content_[name];
return null; // doesn't exist
}
Je retourne un objet s'il existe ou null
sinon.
Quel serait l'équivalent en C++ en utilisant des références? Y a-t-il un schéma recommandé en général? J'ai vu quelques frameworks ayant une méthode isNull()
à cet effet:
SomeResource SomeClass::getSomething(std::string name) {
if (content_.find(name) != content_.end()) return content_[name];
SomeResource output; // Create a "null" resource
return output;
}
Ensuite, l'appelant vérifierait la ressource de cette façon:
SomeResource r = obj.getSomething("something");
if (!r.isNull()) {
// OK
} else {
// NOT OK
}
Cependant, devoir implémenter ce type de méthode magique pour chaque classe semble lourd. De plus, il ne semble pas évident quand l'état interne de l'objet doit être réglé de "null" à "not null".
Existe-t-il une alternative à ce modèle? Je sais déjà que cela peut être fait à l'aide de pointeurs, mais je me demande comment/si cela peut être fait avec des références. Ou dois-je abandonner le retour des objets "null" en C++ et utiliser un modèle spécifique à C++? Toute suggestion sur la manière appropriée de le faire serait appréciée.
Vous ne pouvez pas le faire pendant les références, car elles ne doivent jamais être NULL. Il existe essentiellement trois options, l'une utilisant un pointeur, les autres utilisant la sémantique des valeurs.
Avec un pointeur (remarque: cela nécessite que la ressource ne soit pas détruite pendant que l'appelant a un pointeur dessus; assurez-vous également que l'appelant sait qu'il n'a pas besoin de supprimer l'objet):
SomeResource* SomeClass::getSomething(std::string name) {
std::map<std::string, SomeResource>::iterator it = content_.find(name);
if (it != content_.end())
return &(*it);
return NULL;
}
En utilisant std::pair
avec un bool
pour indiquer si l'élément est valide ou non (remarque: nécessite que SomeResource ait un constructeur par défaut approprié et ne soit pas coûteux à construire):
std::pair<SomeResource, bool> SomeClass::getSomething(std::string name) {
std::map<std::string, SomeResource>::iterator it = content_.find(name);
if (it != content_.end())
return std::make_pair(*it, true);
return std::make_pair(SomeResource(), false);
}
En utilisant boost::optional
:
boost::optional<SomeResource> SomeClass::getSomething(std::string name) {
std::map<std::string, SomeResource>::iterator it = content_.find(name);
if (it != content_.end())
return *it;
return boost::optional<SomeResource>();
}
Si vous voulez une sémantique de valeur et avoir la possibilité d'utiliser Boost, je recommanderais l'option trois. Le principal avantage de boost::optional
plus de std::pair
est qu'un _ boost::optional
valeur ne construit pas le type de son encapsulation. Cela signifie qu'il fonctionne pour les types qui n'ont pas de constructeur par défaut et économise du temps/de la mémoire pour les types avec un constructeur par défaut non trivial.
J'ai également modifié votre exemple afin que vous ne recherchiez pas la carte deux fois (en réutilisant l'itérateur).
Pourquoi "en plus d'utiliser des pointeurs"? Utiliser des pointeurs is comme vous le faites en C++. Sauf si vous définissez un type "facultatif" qui a quelque chose comme la fonction isNull()
que vous avez mentionnée. (ou utilisez-en un existant, comme boost::optional
)
Les références sont conçues et garanties pour ne jamais être nul . Demander "alors comment les rendre nuls" n'a pas de sens. Vous utilisez des pointeurs lorsque vous avez besoin d'une "référence nullable".
Une approche agréable et relativement non intrusive, qui évite le problème si vous implémentez des méthodes spéciales pour tous les types, est celle utilisée avec boost.optional . Il s'agit essentiellement d'un wrapper de modèle qui vous permet de vérifier si la valeur détenue est "valide" ou non.
BTW Je pense que cela est bien expliqué dans la documentation, mais méfiez-vous de boost::optional
de bool
, c'est une construction difficile à interpréter.
Edit : La question concerne la "référence NULL", mais l'extrait de code a une fonction qui renvoie par valeur. Si cette fonction a en effet renvoyé une référence:
const someResource& getSomething(const std::string& name) const ; // and possibly non-const version
alors la fonction n'aurait de sens que si le someResource
auquel on se référait avait une durée de vie au moins aussi longue que celle de l'objet renvoyant la référence (sinon vous auriez une référence pendante). Dans ce cas, il semble parfaitement correct de renvoyer un pointeur:
const someResource* getSomething(const std::string& name) const; // and possibly non-const version
mais vous devez faire absolument clair que l'appelant ne s'approprie pas le pointeur et ne doit pas tenter de le supprimer.
Je peux penser à quelques façons de gérer cela:
boost::optional
contrairement à Java et C # dans l'objet de référence C++ ne peuvent pas être nuls.
donc je conseillerais 2 méthodes que j'utilise dans ce cas.
1 - au lieu de référence, utilisez un type qui a un null tel que std :: shared_ptr
2 - obtenir la référence en tant que paramètre de sortie et renvoyer un booléen pour réussir.
bool SomeClass::getSomething(std::string name, SomeResource& outParam) {
if (content_.find(name) != content_.end())
{
outParam = content_[name];
return true;
}
return false;
}
Ce code ci-dessous montre comment renvoyer des références "non valides"; c'est juste une manière différente d'utiliser les pointeurs (la méthode conventionnelle).
Il n'est pas recommandé d'utiliser cela dans du code qui sera utilisé par d'autres, car les fonctions qui renvoient des références renvoient toujours des références valides.
#include <iostream>
#include <cstddef>
#define Nothing(Type) *(Type*)nullptr
//#define Nothing(Type) *(Type*)0
struct A { int i; };
struct B
{
A a[5];
B() { for (int i=0;i<5;i++) a[i].i=i+1; }
A& GetA(int n)
{
if ((n>=0)&&(n<5)) return a[n];
else return Nothing(A);
}
};
int main()
{
B b;
for (int i=3;i<7;i++)
{
A &ra=b.GetA(i);
if (!&ra) std::cout << i << ": ra=nothing\n";
else std::cout << i << ": ra=" << ra.i << "\n";
}
return 0;
}
La macro Nothing(Type)
renvoie un valeur, dans ce cas celui représenté par nullptr
- vous pouvez également utiliser 0
, Auquel l'adresse de la référence est défini. Cette adresse peut maintenant être vérifiée comme si vous utilisiez des pointeurs.
Voici quelques idées:
Alternative 1:
class Nullable
{
private:
bool m_bIsNull;
protected:
Nullable(bool bIsNull) : m_bIsNull(bIsNull) {}
void setNull(bool bIsNull) { m_bIsNull = bIsNull; }
public:
bool isNull();
};
class SomeResource : public Nullable
{
public:
SomeResource() : Nullable(true) {}
SomeResource(...) : Nullable(false) { ... }
...
};
Alternative 2:
template<class T>
struct Nullable<T>
{
Nullable(const T& value_) : value(value_), isNull(false) {}
Nullable() : isNull(true) {}
T value;
bool isNull;
};
Encore une autre option - une que j'ai utilisée de temps en temps pour quand vous ne voulez pas vraiment qu'un objet "null" soit retourné mais à la place un objet "vide/invalide" fera:
// List of things
std::vector<some_struct> list_of_things;
// An emtpy / invalid instance of some_struct
some_struct empty_struct{"invalid"};
const some_struct &get_thing(int index)
{
// If the index is valid then return the ref to the item index'ed
if (index <= list_of_things.size())
{
return list_of_things[index];
}
// Index is out of range, return a reference to the invalid/empty instance
return empty_struct; // doesn't exist
}
Son assez simple et (en fonction de ce que vous faites avec lui à l'autre extrémité) peut éviter d'avoir à faire des vérifications de pointeur nul de l'autre côté. Par exemple, si vous générez des listes de choses, par exemple:
for (const auto &sub_item : get_thing(2).sub_list())
{
// If the returned item from get_thing is the empty one then the sub list will
// be empty - no need to bother with nullptr checks etc... (in this case)
}