C++ and returning a null - what worked in Java doesn't work in C++
So I'm having a rather tumultuous conversion to C++ from Java/C#. Even though I feel like I understand most of the basics, there are some big fat gaping holes in my understanding.
For instance, consider the following function:
Fruit&
FruitBasket::getFruitByName(std::string fruitName)
{
std::map<std::string,Fruit>::iterator it = _fruitInTheBascit.find(fruitName);
if(it != _fruitInTheBascit.end())
{
return (*it).second;
}
else
{
//I would so love to just return null here
}
}
Where _fruitsInTheBascit
is a std::map<std::string,Fruit>
. If I query getFruitByName("kumquat")
you know it's not going to be there - who eats kumquats? But I don't want my program to crash. What shou开发者_开发百科ld be done in these cases?
P.S. tell me of any other stupidity that I haven't already identified.
There is no such thing in C++ as a null reference, so if the function returns a reference, you can't return null. You have several options:
Change the return type so that the function returns a pointer; return null if the element is not found.
Keep the reference return type but have some sort of "sentinel" fruit object and a return a reference to it if the object is not found.
Keep the reference return type and throw an exception (e.g.,
FruitNotFoundException
) if the fruit is not found in the map.
I tend to use (1) if a failure is likely and (3) if a failure is unlikely, where "likely" is a completely subjective measure. I think (2) is a bit of a hack, but I've seen it used neatly in some circumstances.
As an example of an "unlikely" failure: in my current project, I have a class that manages objects and has a function is_object_present
that returns whether an object is present and a function get_object
that returns the object. I always expect that a caller will have verified the existence of an object by calling is_object_present
before calling get_object
, so a failure in this case is quite unlikely.
OK. Lots of solutions.
James McNellis has covered all the obvious ones.
Personally I prefer his solution (1) but there are a lot of details missing.
An alternative (and I throw it out just as an alternative) is to create a Fruit reference type that knows if the object is valid. Then you can return this from your getFruitByName() method:
Basically it is the same as returning a pointer; BUT there is no ownership symantics associated with a pointer and thus it is hard to tell if you are supposed to delete the pointer. By using the fruit reference type you are not exposing the pointer so it leads to no confusion about the ownership.
class FruitReference
{
public:
FruitReference() // When nothing was found use this.
:data(NULL)
{}
FruitReference(Fruit& fruit) // When you fidn data.
:data(&fruit)
{}
bool isValid() const { return data != NULL;}
Fruit& getRef() const { return *data; }
private:
Fruit* data; //(not owned)
};
FruitReference const& FruitBasket::getFruitByName(std::string fruitName)
{
std::map<std::string,Fruit>::iterator it = _fruitInTheBascit.find(fruitName);
if(it != _fruitInTheBascit.end())
{
return FruitReference((*it).second);
}
else
{
return FruitReference();
}
}
I am sure boost has somthing similar but I could not find it in my 20 second search.
If you need NULL, you can return a pointer instead of a reference.
References cannot be null. They work best with exceptions - instead of returning an error code, you can throw.
Alternatively, you can use an "out" parameter, with an error-code return value:
bool FruitBasket::getFruitByName(const std::string& fruitName, Fruit& fruit)
{
std::map<std::string,Fruit>::iterator it = _fruitInTheBascit.find(fruitName);
if(it != _fruitInTheBascit.end())
{
fruit = (*it).second;
return true;
}
else
{
return false;
}
}
Then call it like this:
Fruit fruit;
bool exists = basket.getFruitByName("apple", fruit);
if(exists)
{
// use fruit
}
The reason this doesn't work is because your function returns a reference. Reference must always be actual instances. Java is not C++.
One way you could fix this is to change the function to return a pointer, which work much more like the references java uses. In that case, you can just return null;
.
Fruit*
FruitBasket::getFruitByName(std::string fruitName)
{
std::map<std::string,Fruit>::iterator it = _fruitInTheBascit.find(fruitName);
if(it != _fruitInTheBascit.end())
{
return &(*it).second;
}
else
{
return NULL;
}
}
If you'd like to avoid doing that, for some reason, you could define a sentinel object and return that instead. something like this
Fruit NullFruit;
Fruit&
FruitBasket::getFruitByName(std::string fruitName)
{
std::map<std::string,Fruit>::iterator it = _fruitInTheBascit.find(fruitName);
if(it != _fruitInTheBascit.end())
{
return (*it).second;
}
else
{
return NullFruit;
}
}
an additional option is to not return at all. Raise an exception
class NullFruitException: public std::exception {};
Fruit&
FruitBasket::getFruitByName(std::string fruitName)
{
std::map<std::string,Fruit>::iterator it = _fruitInTheBascit.find(fruitName);
if(it != _fruitInTheBascit.end())
{
return (*it).second;
}
else
{
throw NullFruitException;
}
}
James McNellis' answer hits it spot-on. I would point out, however, that you should be thinking of C++'s pointers as being like Java's references rather than C++'s references being like Java's references. What you'd do in your situation is similar to what you'd do in Java if you were trying to return a primitive type which can't be null. At that point, you're basically doing what James suggested:
Make it pointer (for a primitive in Java, that would mean using a wrapper class such as Integer or Float, but here you would just use a pointer). However, beware of the fact that you can't return pointers to variables on the stack unless you want big trouble, since the memory will go away when the function call has completed.
Throw an exception.
Create an ugly sentinel value to return (which I would almost always argue is a bad idea).
There are likely other solutions, but those are the key ones, and James did a good job covering them. However, I do feel the need to point out that if you think of objects on the stack in a manner similar to primitives in Java, then it will be easier for you to figure out how to deal with them. And it's good to remember that C++'s references and Java's references are two entirely different beasts.
An object that can be null or invalid is sometimes caused a Fallible object. One example implementation I created can be found here: Fallible.h. Another example would be boost::optional.
i'm a bit rusty in the C++ department. But is it possible to return Fruit* instead of Fruit& ?
If you make this change then you can say "return null;" (or return NULL, or return 0... whatever the syntax is for C++).
精彩评论