[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)