Why use boost::optional when I can return a pointer [closed]
Asked Answered
P

4

9

If I have a find function that can sometimes fail to find the required thing, I tend to make that function return a pointer such that a nullptr indicates that the thing was not found.

E.g.

Student* SomeClass::findStudent(/** some criteria. */)

If the Student exists, it will return a pointer to the found Student object, otherwise it will return nullptr.

I've seen boost::optional advocated for this purpose as well. E.g. When to use boost::optional and when to use std::unique_ptr in cases when you want to implement a function that can return "nothing"?

My question is, isn't returning a pointer the best solution in this case. i.e. There is a possibility that the queried item will not be found, in which case returning nullptr is a perfect solution. What is the advantage of using something like boost::optional (or any other similar solution)?

Note that, in my example, findStudent will only ever return a pointer to an object that is owned by SomeClass.

Pyxis answered 9/2, 2016 at 15:59 Comment(13)
How about the case where your find function returns a value type, e.g. an int or double?Jehovist
Also, from your linked question, https://mcmap.net/q/430029/-when-to-use-boost-optional-and-when-to-use-std-unique_ptr-in-cases-when-you-want-to-implement-a-function-that-can-return-quot-nothing-quotJehovist
@Jehovist Does that make sense? You'd just return the double by value right? If you wanted the user to be able to edit a particular double in memory owned by SomeClass, then you'd again return double*.Pyxis
@Pyxis If the function returns double by value (or, better, int by value), and you want to indicate you haven't found it, you'll return ... what?Sanity
@nappyfalcon: the whole question was about optional. what if the value wasn't found?Hearts
@Pyxis With a pointer it's not particularly clear who's responsible for its deletion, and that it can be null and what that means. optional at least makes things clearer for a user of an API.Edette
Ok, regarding the double - so a possible answer is "when you want to return something by value but still want to indicate not found." Fine. But what about the general case?Pyxis
@Biffen, I'm going by the rule that unless otherwise stated in big letters, the creator is responsible for deletion.Pyxis
@Pyxis It's not clear (to me at least) who the creator is here. Does findStudent() create a new object? I'd guess not, but the API doesn't convey it.Edette
@nappyfalcon: what happens when the API user forgets that the value is optional and dereferences nullptr?Hearts
@Edette It doesn't matter to the invoker. But it should be clear that the invoker of findStudent is not expected to own the returned instance. And if you think it's not so clear, then does the use of boost::optional really solve that?Pyxis
@nappyfalcon: optional solves this problem either by copying-by-value the result to the caller (or possibly moving it in C++11). Ownership now rests with the caller in the form of a temporary, which is then destroyed in a way that has clear semanticsLaureen
@Pyxis I'm just trying to answer the question, and as far as I can see, the greatest advantage of optional is not so much a technical one, as just being clearer to developers that it's a type that may or may not have a value. Pointers have a lot of drawbacks. I won't list them here, but optional at least has RAII, which (raw) pointers lack. Perhaps you're presenting the perfect use-case for pointers, but optional has others, in some people's opinions.Edette
C
11

The advantage of an optional<Student&> return type here is that the semantics of usage are readily apparent to all users that are familiar with optional (and will become readily apparent once they familiarize themselves with it). Those semantics are:

  • The caller does not own the Student and is not responsible for memory management. The caller simply gets a reference to an existing object.
  • It is clear that this function can fail. You maybe get a value and you maybe get nothing. It is clear that the caller needs to check the result one way or the other.

optional<T> is self-documenting in a way that T* isn't. Moreover, it has other benefits in that it can work in cases where you want to return any kind of object type without the need for allocation. What if you needed to return an int or double or SomePOD?

Circumspection answered 9/2, 2016 at 16:18 Comment(7)
optional<T&> is explicitly illegal in the current TS version of the type. optional is for value-types.Leishmaniasis
Hm, that would require optional<reference_wrapper<T>>, then. Ugly.Sanity
@Angew make_optional(std::ref(x))Commoner
@Commoner I meant when specifying the return type. Let's assume auto is not an option there. You can do some magic with decltype, but it's ugly too.Sanity
@Barry: Because we have that already; it's called a "pointer". optional is for value types.Leishmaniasis
@NicolBolas optional is for maybe types. Why the unnecessary restriction to values? Besides, how is there even a consideration for observer_ptr<T> then?Circumspection
@Barry: "Besides, how is there even a consideration for observer_ptr<T> then?" Note that the Core C++ Guidelines go the other way on pointer types. That is, T* is assumed to be non-owning, and if you need to represent an owned pointer without a smart pointer, then you use owned<T>.Leishmaniasis
G
7

optional<T&> was removed from the C++ standardization track because its use is questionable: it behaves nearly identically to a non-owning T* with slightly different (and confusingly different from optional<T> and T*) semantics.

optional<T&> is basically a non-owning T* wrapped up pretty, and somewhat strangely.


Now, optional<T> is a different beast.

I have used optional<Iterator> in my container-based find algorithms. Instead of returning end(), I return the empty optional. This lets users determine without a comparison if they have failed to find the item, and lets code like:

if(linear_search_for( vec, item))

work, while the same algorithm also lets you get at both the item and the location of the item in the container if you actually need it.

Pointers to elements doesn't give you the location information you might want except with contiguous containers.

So here, I've created a nullable iterator that has the advantages of iterators (generically working with different types of containers) and pointers (can be tested for the null state).

The next use is actually returning a value. Suppose you have a function that calculates a rectangle.

Rect GetRect();

now, this is great. But what if the question can be meaningless? Well, one approach is to return an empty rect or other "flag" value.

Optional lets you communicate that it can return a rect, or nothing, and not use the empty rect for the "nothing" state. It makes the return value nullable.

int GetValue();

is a better example. An invalid value could use a flag state of the int -- say -1 -- but that forces every user of your function to look up and track the flag state, and not accidentally treat it as a normal state.

Instead, optional<int> GetValue() makes it clear that it can fail, and what the failure state it. If it is populated, you know it is a real value, and not a flag value.

In both of these cases, returning a non-owning pointer is non-viable, because who owns the storage? Returning an owning pointer is expensive, because pointless heap allocations are pointless.

Optionals are nullable value types. When you want to manage resources locally, and you still want an empty state, they make it clear.


Another thing to look into is the expected type being proposed. This is an optional, but when in the empty state contains a reason why it is empty.

Grillwork answered 9/2, 2016 at 16:39 Comment(1)
I like "because pointless heap allocations are pointless" - "because dumb is dumb". Well said, sir!Delinda
D
3

optional<T&> may indeed be replaced by T* but T* has not clear semantic (ownership ?).

But optional<T> cannot be replaced by T*. For example:

optional<Interval> ComputeOverlap(const Interval&, const Interval&);

If there is no overlap, no problem with T* (nullptr) or optional<T>. But if there is an overlap, we need to create a new interval. We may return a smart_pointer in this case, or optional.

Darice answered 9/2, 2016 at 16:34 Comment(0)
U
2

Lets consider you have a std::map<IndexType, ValueType> where you are trying to find something (Note: The same applies for other containers, this is just to have an example). You have these options:

  • You return a ValueType&: The user can modify your map-content and does not need to think about memory-allocation/deallocation. But if you dont find anything in your map, you need to throw an exception or something similar.
  • You return a ValueType*: The user can modify your map-content and you can return a nullptr if you dont find anything. But the user can call delete on that pointer and you must specify anyhow if he has to do so or not.
  • You return a smart pointer to ValueType: The user does not have to worry about delete or not-delete and can modify your map-content depending on the type of smart-pointer. You can also return a nullptr. But this pretty much requires you to deal with smart_pointers in your map, which is overly complicated if ValueType would be e.g. just an int otherwise.
  • You return a simple ValueType: The user can not modify your map-content and does not need to think about memory-allocation/deallocation. But if you dont find anything in your map, you need to return some special ValueType which tells the user you didn't find anything. In case your ValueType is e.g. int, which one would you return that makes clear "no int found".
  • You return a boost::optional, which is the closest you can get to a simple ValueType return by value with the additional option of "not returning a ValueType"
Uxmal answered 9/2, 2016 at 16:20 Comment(1)
For the first 2 cases: you can return const reference or pointer, so that the user cannot modify itBaram

© 2022 - 2025 — McMap. All rights reserved.