boost::optional For Pointers

Started by
38 comments, last by Hodgman 13 years, 7 months ago
Quote:Original post by cache_hit
I get it, trust me. I just think that simplicity is a far more noble goal to strive for than consistency.

Anyway, what does the following code snippet print?

*** Source Snippet Removed ***


It'll print 2, but I think that's beside the point because there's no reasonable use case where you'd end up with the equivalent of optional<ptr>(0), unless you make it yourself... but why would you? I guess we'll have to disagree with regard to simplicity vs. consistency, though. I'm of the mind that I'd rather do something the same way everywhere and have it be slightly more verbose, than do it differently in different contexts and slightly less verbose. Obviously when it gets extreme one way or the other things are different, but I don't think this is that extreme. I also always prefer to have the code be the documentation (in addition to regular documentation) wherever possible.

Two different approaches to programming, I suspect.
Advertisement
Quote:Original post by Shinkage
Quote:Original post by Zipster
typedef Foo* NullableFooPtr; // Can be NULL, check!
typedef Foo* NonNullableFooPtr; // Can't be NULL if you get far enough to have one, no need to check

I don't like this solution because it seems to be saying that null-ability is a property of the pointer, as opposed to being a property of its usage. I think the semantics of optional<FooPtr> are closer to what's really going on, but that may just be me.

The semantics of optional<FooPtr> are that the pointer's existence is optional. You've tacked on additional semantics, namely that if a pointer exists you need to check for NULL, and that the lack of boost::optional indicates the pointer is only NULL in exceptional circumstances and you don't need to check. While all that additional meaning may make sense to you it's not something most other people will pick up on immediately (as evidenced by the number of times it's needed to be reiterated), so if your intent is to make the code as clear and self-documenting as possible I think you're going about it the wrong way.

However the concept of "nullable" values has been around in database systems for a while, and proper usage patterns when dealing with these values is quite clear. If something is nullable, it means it can be NULL and you need to check. Otherwise you can assume it isn't. If I understood what you were trying to do correctly, this is one of the most direct ways to convey that notion to the user. It's a property of the value, and usage will follow from that. As for the technical details of conversion, etc., you would need to handle that if you were looking for a formal solution, but if your goal is syntactic sugar then the typedefs should work fine.
Quote:Original post by Shinkage
Quote:Original post by cache_hit
I get it, trust me. I just think that simplicity is a far more noble goal to strive for than consistency.

Anyway, what does the following code snippet print?

*** Source Snippet Removed ***


It'll print 2, but I think that's beside the point because there's no reasonable use case where you'd end up with the equivalent of optional<ptr>(0), unless you make it yourself... but why would you? I guess we'll have to disagree with regard to simplicity vs. consistency, though. I'm of the mind that I'd rather do something the same way everywhere and have it be slightly more verbose, than do it differently in different contexts and slightly less verbose. Obviously when it gets extreme one way or the other things are different, but I don't think this is that extreme. I also always prefer to have the code be the documentation (in addition to regular documentation) wherever possible.

Two different approaches to programming, I suspect.


The fact that it does print 2 is one of the biggest reasons I wouldn't want to use it. In that respect it is ANTI consistency with well-established usage practices. I still just don't see anything to gain from doing this. You're trying to circumvent one of the fundamental features of the language and having no tangible benefit to show for it.

I mean I know you probably don't believe me when I say I get it, but I really do. But after abusing boost long enough, you really start to adopt more of a "less is more" philosophy. Happened with me and it happened with everyone else I've known who's used boost heavily.
Quote:I also always prefer to have the code be the documentation (in addition to regular documentation) wherever possible.


X * foo();

This function may return null. There is no need to document it, it's an established fact. Since pointers can be null, the return value may be null.

Quote:there's no reasonable use case where you'd end up with the equivalent of optional<ptr>(0)


Fallacy: optional prevents returning uninitialized local as per this example:
X * foo() {  X * temp;  if (bar) temp = new X();  return temp;}

It helps a bit, but doesn't solve the problem by far:
optional<X> * foo() {  X * temp;  if (bar) temp = new X();  return optional<X*>(temp);}
Now return value is valid, but pointer is not.

Ideally, one would use 'optional<X*> temp;', but this is as reasonable as expecting every variable in C or C++ to be properly initialized.

The above falls very much into reasonable use case, since X is highly likely to be some third-party raw pointer or foo() might be a complex function which will not be able to enforce optional<> in entirety.


While I agree that in ideal world optional<> and other helpers would allow code to be perfectly correct with no possibility of invalid pointers, sooner or later one will touch third-party non-boost code which doesn't use it, thereby breaking the consistency.

It becomes problematic when dealing with C APIs which idiomatically return NULL on failure as well as C++ APIs which use non-throw allocator.
Also, it breaks generic programming in the sense that you can no longer treat optional<T*> and T* the same for algorithms and such, when conceptually they are both just pointers. If you have an std::vector<optional<T*> > and an std::vector<T*>, you can't use the same algorithms on them.

Furthermore, in an std::vector<optional<T*>>, the actual pointers aren't adjacent in memory, which will be poor for cache performance. This is in addition to the C interoperability benefits you lose such as those Antheus pointed out.

Ultimately you lose a number of tangible, easily quantifiable benefits, while gaining something that is arguable at best.
Quote:Original post by Antheus
Fallacy: optional prevents returning uninitialized local as per this example:

I think you misunderstood me. The library will never produce an optional containing a null or uninitialized pointer. It's just designed that way. Could the user start filling optionals with null pointers on their own? Sure, I suppose so, but I'm not sure why.

That does give me the idea though for further specialization. In addition to automatically handling the double-indirection (so you don't have to (*optional)-> to get at the pointer), I could give the constructor of pointer-optionals a special case that makes constructing with a null pointer equivalent to default constructing an empty optional. That might actually be a good idea... although I'll have to think of whether it might have unintended consequences...

Quote:Original post by Antheus
While I agree that in ideal world optional<> and other helpers would allow code to be perfectly correct with no possibility of invalid pointers, sooner or later one will touch third-party non-boost code which doesn't use it, thereby breaking the consistency.

It becomes problematic when dealing with C APIs which idiomatically return NULL on failure as well as C++ APIs which use non-throw allocator.
Quote:Original post by cache_hit
Also, it breaks generic programming in the sense that you can no longer treat optional<T*> and T* the same for algorithms and such, when conceptually they are both just pointers. If you have an std::vector<optional<T*> > and an std::vector<T*>, you can't use the same algorithms on them.

I'm starting to think you're all assuming much more widespread use of optional than I intended to indicate. Really, it just appears almost entirely as a wrapper around function return values. More than that, after the whole "optional" part is dealt with, the user is free to discard the wrapper and use the wrapped pointer by itself. It's nothing like, "if a value is returned as an optional, it has to stay like that forever."
I guess to me that makes it even weirder then, since you're going out of your way to impose this constraint via the type system, and then saying that it's ok to circumvent it as you see fit. If it were me, I would write the following function:

template<class T*>T* extract(const boost::optional<T*>& o){   return (t) ? t.get() : NULL;}


And everywhere I encountered a function similar to:

boost::optional<T*> foo(){   //...}


I would invoke it as:

T* p = extract(foo());


If this is just for you, go ahead and do what you will with it, but I'm just telling you how this is going to be used in a real situation with real programmers who are just trying to GSD without seemingly pointless annoyances getting in their way. Not trying to be harsh, but the above is what's going to happen with a lot of people using such a thing.
Um... why would you invoke it like that? How is that possibly any simpler than invoking it like so:
optional<T*> p = foo();
...other than being all of one character less to type? "GSD" as you like to put it is great, but I'd modify it to "GSD the right way," and the point here is to help make "the right way" more obvious and harder to avoid.
Quote:Original post by Shinkage
Um... why would you invoke it like that? How is that possibly any simpler than invoking it like so:
optional<T*> p = foo();
...other than being all of one character less to type? "GSD" as you like to put it is great, but I'd modify it to "GSD the right way," and the point here is to help make "the right way" more obvious and harder to avoid.


Because I like dealing with pointers, not things pretending to be pointers unless they actually ARE smart pointers and have the same interface as a pointer.

This thing just doesn't actually do anything. It's not a smart pointer. You have to write ugly syntax like p.get()->foo() if you want to do anything because it doesn't have an overloaded operator->. "if (p)" doesn't do what you'd expect.

Anyway, I've said what I need to say. You said specifically in your original post

Quote:
I can't help but think it might really annoy some C++ programmers out there. So, does anybody out there think this convention is overly cumbersome/excessive? Or even more importantly, counterproductive in some way?


and then you're upset that I'm replying with an answer of "yes". So idk what else I can say except that judging by the rest of the thread it doesn't seem like I'm alone. Use that information how you will.
This thread has actually been helpful in refining how I use it. Thanks everyone!

This topic is closed to new replies.

Advertisement