[HN Gopher] T* Makes for a Poor Optional
       ___________________________________________________________________
        
       T* Makes for a Poor Optional
        
       Author : todsacerdoti
       Score  : 65 points
       Date   : 2021-12-13 17:01 UTC (5 hours ago)
        
 (HTM) web link (brevzin.github.io)
 (TXT) w3m dump (brevzin.github.io)
        
       | kentonv wrote:
       | Note the original title is: T* makes for a poor optional<T&>
       | 
       | The last `<T&>` there is key, but seems to have been removed from
       | the HN version of the title.
       | 
       | This blog post is not arguing for the use of optionals in general
       | (it assumes everyone already agrees with that). It is instead
       | making a much more wonky argument that std::optional<T> should
       | support T being a reference type, which apparently is not
       | supported right now.
       | 
       | For most purposes, it seems std::optional<T&> would behave very
       | similarly T*, but the ergonomics would be better in certain
       | template scenarios. It seems like a valid argument to me, but
       | definitely getting into the weeds a bit.
       | 
       | Aside: IMO std::optional in general is disappointing because it
       | doesn't actually solve the worst problem with nullable pointers:
       | dereferencing a null std::optional is still undefined behavior. I
       | wish the standard had gone with a design that forces the
       | developer to write an explicit check when unwrapping the
       | optional...
        
         | nwallin wrote:
         | > Aside: IMO std::optional in general is disappointing because
         | it doesn't actually solve the worst problem with nullable
         | pointers: dereferencing a null std::optional is still undefined
         | behavior. I wish the standard had gone with a design that
         | forces the developer to write an explicit check when unwrapping
         | the optional...
         | 
         | You can use `.value()` to deref an optional instead of
         | `operator*` or `operator->`. It will throw an exception on
         | empties.
         | 
         | IMHO the primary purpose for a function returning a
         | `std::optional` instead of either returning a value on success
         | or throwing an exception on failure is that you can use it in
         | codebases where exceptions are disabled for whatever reason. If
         | the behaviors of `.value()` vs `operator*` were swapped, such
         | that `operator*` throws exceptions and `.value()` is undefined
         | behavior, all the people who wanted it for the purposes of
         | eschewing exceptions, the crowd who wanted `std::optional` the
         | most, would be getting the verbose code.
        
           | kentonv wrote:
           | Throwing an exception is somewhat better but still not what I
           | really want, usually. I want the compiler to make me write an
           | if()-like statement checking if the value is non-null before
           | I can dereference it.
        
           | masklinn wrote:
           | > You can use `.value()`
           | 
           | Which is bad at every level if resolution: it's more verbose
           | to do "the right thing" (obviously), it's more expensive to
           | do "the right thing" (because if you've properly checked
           | you're paying the check twice and praying the compiler can
           | remove the redundant check), and it's incompatible with other
           | pointers to do "the right thing" (because pointers use ! And
           | *).
           | 
           | So .value() it mostly worthless, if it's being used it's an
           | assertion with overhead which can take down your program.
           | 
           | > all the people who wanted it for the purposes of eschewing
           | exceptions, the crowd who wanted `std::optional` the most,
           | would be getting the verbose code.
           | 
           | And nothing of value would be lost (except for it not
           | behaving as a "standard pointer" anymore).
        
             | kllrnohj wrote:
             | > it's more expensive to do "the right thing" (because if
             | you've properly checked you're paying the check twice and
             | praying the compiler can remove the redundant check)
             | 
             | Eh that's hardly a solid complaint here. Since these are
             | header-defined templates, it's all inlined. And any
             | compiler will absolutely remove those redundant checks as a
             | result. And an unchecked .value() would still be available
             | if you really did object to that "overhead", so it doesn't
             | violate any C++ principals here.
             | 
             | I do kinda think that operator-> & operator* as a result
             | did the assertion, and value() bypassed it. But this would
             | also be the opposite behavior of other std:: classes, like
             | std::vector (operator[] being unchecked vs. at() being
             | checked).
        
         | strager wrote:
         | > I wish the standard had gone with a design that forces the
         | developer to write an explicit check when unwrapping the
         | optional...
         | 
         | If this is how std::optional worked, hardly anyone would use
         | it.
        
           | winter_squirrel wrote:
           | Why, other languages that require unwrapping it still see
           | plenty of people using optional. In fact, I would have more
           | inclination to use it in my projects if that was the only way
           | to do it.
        
           | kamray23 wrote:
           | that is how it works everywhere else and that is how it is
           | used everywhere else
        
           | brandmeyer wrote:
           | To be fair, languages which require explicit unwrapping also
           | provide syntactic short-hands for the most common idioms.
           | Long-handed if/else (or pattern matching) is used to
           | implement the short-hands or as a last resort.
           | 
           | See also value_or and the C++20 monadic methods on
           | std::optional.
           | 
           | http://open-
           | std.org/JTC1/SC22/WG21/docs/papers/2021/p0798r6....
        
           | kentonv wrote:
           | > If this is how std::optional worked, hardly anyone would
           | use it.
           | 
           | OK... Well, this is how kj::Maybe works and having written
           | several hundred thousand lines of code using it, I think it
           | works well.
           | 
           | https://github.com/capnproto/capnproto/blob/master/kjdoc/tou.
           | ..
        
             | kllrnohj wrote:
             | Well, that seems to be using macros to cover up the syntax,
             | which of course the C++ committee isn't going to recommend
             | or use for anything new (nor should they).
             | 
             | The actual implementation of Maybe doesn't seem to force
             | anything, it just returns a pointer to the stored value.
             | There's no enforcement of any kind that anything is checked
             | as a result. There's some hoop-jumping with friend classes
             | to "hide" the readMaybe() function to make the non-macro
             | usage as ugly as possible to strongly encourage using the
             | macro-convention, but that's hardly "enforcement".
             | 
             | It's a cute syntax & macro pattern, but it's hardly a good
             | fit for a C++ standard, either.
        
       | kazinator wrote:
       | Well, I mean look, T* is a poor optional<T> simply because an
       | optional<T> behaves like a T when present, whereas a T* makes no
       | pretense that it's not a T.
       | 
       | If X isn't Y, and doesn't even try to sort of be a Y when you
       | need it to in useful circumstances, then X is a poor optional Y
       | for that excellent reason.
       | 
       | A tail is a poor optional leg, for starters, and so on.
       | 
       | What you need is some_kinda_smart_pointer<T> instead of T*, which
       | can be T* when you need it to, but also quacks like T most of the
       | time; like when passed as an argument to a function needing a T,
       | it will convert to that by yielding the T value and so forth.
       | 
       | The name "some_kinda_smart_pointer" can be "optional", and QED.
       | 
       | A T* isn't a class type; it's a basic type: a pointer. You can't
       | do too many clever things with basic types in C++. They can't
       | have any member functions, so they cannot substitute anywhere
       | where something is required that has member functions.
       | 
       | TLDR: naked pointers inherited from C have disadvantages compared
       | to smart pointers in all sorts of generic programming and
       | whatnot; ask any C++98 programmer.
        
         | kllrnohj wrote:
         | I think you greatly misunderstood the article. There's no issue
         | with optional<T>, nor is the article complaining that T* is a
         | bad optional<T>.
         | 
         | The problem is that you can't have optional<T&>. So if you want
         | to pass an optional reference to something, you're currently
         | forced to pass T* instead (using null as the proxy for "not
         | passed", of course). And thus, T* makes for a poor
         | optional<T&>.
        
       | assbuttbuttass wrote:
       | go suffers from this problem. Lack of generics makes it
       | impossible to define an optional<T> type, and *T is annoying to
       | use and requires an extra allocation.
       | 
       | In practice, people use a zero value to indicate that a value is
       | missing, but this only works if the zero value isn't also a valid
       | input.
       | 
       | Hopefully the coming generics will resolve this pain point.
        
         | kubb wrote:
         | Generics will let you define an Option[any], just hope that
         | your code reviewers don't decide that this isn't goey, and you
         | should "keep it simple" or "just use a pointer" or something in
         | that vein.
         | 
         | There won't be overloading * for getting the value inside and
         | overloading bool conversion to check if the value is there.
         | Still, it's an improvement.
        
         | SQueeeeeL wrote:
         | One philosophy is that zero input basically never be a valid
         | input unless you're taking in integers.
         | https://www.infoq.com/presentations/Null-References-The-Bill...
         | . The idea is to try and never create null objects. I think
         | it's interesting and helpful in practice to check/fail early.
        
           | samhw wrote:
           | > The idea is to try and never create null objects
           | 
           | This is one of those things which sounds great in practice,
           | and survives approximately 17 seconds of working on a real
           | codebase.
        
             | SQueeeeeL wrote:
             | Yeah, I think this might be a tab/spaces mixing situation
             | like python had, where it had to be restricted by the
             | language compiler because it's just so tempting.
        
           | simiones wrote:
           | > One philosophy is that zero input basically never be a
           | valid input unless you're taking in integers
           | 
           | This is obviously not possible, since you can imagine lots of
           | complex objects are all integers or themselves composed of
           | integers, and for this entire class of objects, the zero
           | value usually makes sense.
           | 
           | For example, a 3-vector (in the physics sense) is a 3-tuple
           | of integers `type Vec3 struct {x, y, z int}` , `Vec3 {0, 0,
           | 0}` being the origin. How does a function specify that it can
           | take an optional 3-vector? Even worse, you can imagine a
           | struct for a potenitally 0-volume cube represented as 8
           | Vec3s, one for each vertex, whose 0 value is again a valid
           | object.
           | 
           | Of course, you could gratuitously add an `IsValid bool` flag
           | which must be true to the class, but the cube example shows
           | how this quickly becomes annoying and bloated.
        
             | SQueeeeeL wrote:
             | In this case, the memory pointer for Vec3 would not be
             | null? It will be a defined memory address which points to 3
             | zeros. I think it's a difference of objects and primitive
             | data types.
        
               | simiones wrote:
               | The discussion was that in Go, given the current lack of
               | generics, you have two options to have an optional
               | parameter:
               | 
               | 1. Take a *Vec3, using nil as the "optional is missing"
               | value.
               | 
               | 2. Take a Vec3, but consider some value as "invalid",
               | often the 0-value as possible.
               | 
               | 1 invokes exactly the issues in Hoare's essay. 2 doesn't
               | work for all types, as some types don't have any bit
               | pattern that are not useful, this is what I was trying to
               | point out.
        
               | SQueeeeeL wrote:
               | I can't think of any situations where (1) would be
               | insufficient, but I'm probably wrong. But I agree that
               | setting an isValid bit for every object is overkill.
        
           | Jtsummers wrote:
           | But many types do have a "natural" zero value, that you may
           | want to use, beyond just the numbers. Consider the empty
           | string, empty list, empty dictionary, empty set. Those are
           | zero values for their types. By reserving them as pseudo-
           | nulls you lose the ability to distinguish between an object
           | which happens to be empty and an object which is meant to
           | mean "null". This is where Optional (or similar mechanisms)
           | come in handy. If you actually want to avoiding creating
           | null, Optional is a cleaner and more consistent way to
           | accomplish this without needing to magic up a null-value in
           | your type's valid set of values or create a one-off Optional
           | (MaybeDictionary, MaybeList).
        
         | wahern wrote:
         | > people use a zero value to indicate that a value is missing
         | 
         | Go supports returning multiple results. Maybe I don't read
         | enough Go code to understand what's _actually_ popular, but
         | AFAIU the idiomatic way to accomplish this in Go is something
         | like:                 if v, exists := lookup(key); exists {
         | do(v)       }
         | 
         | or                 if r, err != foo(); err != nil {
         | do(r)       }
         | 
         | The compiler, unfortunately, can't enforce proper conditionals.
         | OTOH, unlike C compilers Go won't make any dangerous
         | assumptions about dereferencing nil pointers, which is
         | something.
         | 
         | I'm not sure Generics can help here. What you really want is
         | compiler enforcement of comprehensive condition checks. For
         | example, via pattern matching switch statements. That's easiest
         | to do with optional types as the unwrapping operation creates a
         | simple point for static constraint checking, but in principle
         | the same thing could be accomplished with annotations on multi-
         | value return types that describe the association.
        
           | assbuttbuttass wrote:
           | I'm talking about optional inputs, you're right that multi-
           | return works for optional outputs.
           | 
           | How would you write a function that takes a string, but can
           | use some default if none is provided?
           | 
           | Common way:                   func f(s string) {
           | if s == "" {                 s = "default"             }
           | ...         }
        
             | vishvananda wrote:
             | You could always use a pointer for this, but admittedly it
             | is pretty ugly when compared to a true optional type:
             | func f(s *string) string {             ret := ""
             | if s == nil {                 ret = "default"             }
             | else {                 ret = *s             }
             | return ret         }              func main() {
             | s := "foo"             fmt.Println("Hello, " + f(&s))
             | fmt.Println("Hello, " + f(nil))         }
        
       | [deleted]
        
       | formerly_proven wrote:
       | std::optional makes for a poor optional simply because doing the
       | right thing is not the default, requires more typing and uses an
       | interface that doesn't exist in T*.
        
       | psyclobe wrote:
       | std::optional<std::reference_wrapper<T>> is how we deal with this
       | [lovely isn't it?].
        
       | lowbloodsugar wrote:
       | If a developer is committed to using template-fu, then of course
       | T* doesn't fit nicely with all the template magic, and the
       | developer should use a template magic version of T* which is
       | 'optional'. For those who don't like template-fu, T* is way
       | better than 'optional', not simply because 'optional' is a
       | template and therefore bad, but because T* works just fine in
       | this world. So I am confused by the point of this article. It's
       | like saying "gasoline makes a poor diesel when used in my diesel
       | engined car."
        
         | strager wrote:
         | I agree. Just because T* has problems in _some_ contexts doesn
         | 't mean optional<T&> should be used in _all_ context.
        
       | adamc wrote:
       | What is "facially reasonable" supposed to mean?
        
         | steveklabnik wrote:
         | My understanding (not the author): "prima facie" is a legal
         | term for "on its face", and this is saying the same thing but
         | phrased in a different way. It looks reasonable on its face,
         | due to being "superficially similar" (author's words). But when
         | you look a bit deeper, it is not.
        
         | jimminy wrote:
         | It seems like an unusual rephrasing of the saying "reasonable
         | on the face", which means "reasonable upon first appearance,
         | without considering deeper aspects."
         | 
         | It's judging a book by it's cover.
        
         | [deleted]
        
       | isaacimagine wrote:
       | Rust turns an Option<T> into a raw nullable pointer[1] (with NULL
       | being Option::None). This is cool because a 2-item enum with a
       | single pointer would usually be 8 (ptr) + 1 (tag) = 9 (total) [?]
       | 16 (padded) bytes, but it's only 8 because of this.
       | 
       | I'm simplifying a tad, look up 'null pointer specialization rust'
       | for more. What's cool is the zero-cost abstraction over type
       | layout specification, while allowing the generic Option<T> type
       | to work exactly as expected.
       | 
       | [1]: Assuming T is a non-nullable pointer type.
        
         | loeg wrote:
         | > Assuming T is a non-nullable pointer type.
         | 
         | You can even do the same thing for a few special primitive
         | integer types, such as std::num::NonZeroU64.
         | 
         | https://doc.rust-lang.org/std/num/struct.NonZeroU64.html :
         | 
         | > Option<NonZeroU64> is the same size as u64
         | 
         | Behind the scenes, the type is defined with a magic compiler-
         | internal incantation that isn't usable in user code,
         | unfortunately:                   #[repr(transparent)]
         | #[rustc_layout_scalar_valid_range_start(1)]
         | #[rustc_nonnull_optimization_guaranteed]
         | 
         | https://doc.rust-lang.org/src/core/num/nonzero.rs.html#39
        
           | tialaramex wrote:
           | Yes, more of these would be nice, the ones we have are very
           | valuable but it would be nice to make our own, ideally
           | safely, but unsafe ones made carefully in a trustworthy
           | library are good too.
           | 
           | I would also like being able to tell the compiler about other
           | niches. If a Foozle can be Ding, Ping, Zing or Number(n)
           | where I promise n is from 0 to 199, that all fits in a byte
           | _if_ the compiler is willing to work with me by storing Ding,
           | Ping and Zing as magic sentinels in the niche range 200 to
           | 255 and I 'm willing to trade the small runtime cost for the
           | memory usage win.
        
       ___________________________________________________________________
       (page generated 2021-12-13 23:00 UTC)