3

From time immemorial, when passing pointers to or from functions, we tend to special-case the null pointer:

 p = get_pointer_to_the_foo(args);
 if (p == nullptr) { /* foo is inaccessible, do one thing */ }
 else { /* we can access foo, do something else */ }

and this is an inheritance from C. Now, we occasionally would do the same with other types, e.g. using a signed type to represent either a valid non-negative value, and, say, -1 or to indicate an error.

The latter pattern will now be finally deprecated with the onset of std::optional: std::optional<unsigned> is either nullopt or a non-negative value. But - what about pointers? After all, nullptr is just one of innumerable invalid pointer values. So, when writing new code (when all of it is C++17) - should we essentially forget about it, and pass around either std::optional<foo_t*>'s or assumed-non-null foo_t *'s?

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • 4
    `std::optional` doesn't make much sense. Did you mean `std::optional`? – Vittorio Romeo Mar 27 '17 at 11:20
  • @VittorioRomeo: No, I meant what I wrote. Think about it; it does actually make sense. – einpoklum Mar 27 '17 at 11:33
  • `std::optional` means that the pointer itself could not exist. It's "similar" to `foo**`, not to `foo*`. Is that what you meant? – Vittorio Romeo Mar 27 '17 at 11:35
  • Yes. Either you have a pointer - a _valid_ pointer - or no pointer at all. Whereas before we 'simulated' "no pointer" with the nullptr value. – einpoklum Mar 27 '17 at 11:37
  • 3
    Consider `std::optional x`. If `x != nullopt`, there's nothing that guarantees `*x != nullptr`. Why do you assume that `x` would be a *valid* pointer? – Vittorio Romeo Mar 27 '17 at 11:40
  • 2
    In a way all pointers are currently 'optional', i.e. nullptr is considered one of their valid values. If you'd like to clarify that confusion using a type system, you'd want the opposite of `std::optional`. – Kos Mar 27 '17 at 11:41
  • @VittorioRomeo: Oh, but it's exactly the other way around. Now we no longer have to make that silly assumption. We can say that we only ever have valid pointers! We never have to set a pointer to an invalid value again - since we can use nullopt for when we failed to obtain a valid pointer for something. – einpoklum Mar 27 '17 at 11:55
  • @Kos: So what I'm saying is, we can be rid of that anomaly. Stop treating pointers that way. From now on (if we so choose) there are only valid pointers, ever, period. And if you're not sure you can pass a valid pointer, use an `std::optional`. – einpoklum Mar 27 '17 at 11:56
  • 3
    @einpoklum: that's a very optimistic view. Sometimes you don't have control over the pointers that are passed to your function. Also, again, you're adding an extra "layer of optionality" - what you want is `T&` instead of pointers and `optional` for optional references. – Vittorio Romeo Mar 27 '17 at 11:57
  • @VittorioRomeo: It's not an extra layer, it's replacing the existing layer. Also, I'm talking about code that will be written in the future, based on C++17 and onwards (say, maybe the v2 of the standard library). So it's up to us decide how code is to behave. As for invalid pointers being passed - that's also a problem now, if we get `0xDEADBEEF` instead of something meaningful. – einpoklum Mar 27 '17 at 12:40
  • @einpoklum: "*It's not an extra layer, it's replacing the existing layer.*" But it's not replacing anything because *it's still there*. `nullptr` still exists and will be used. `optional t = (T*)nullptr;` is still valid code, and in order for a function that takes `optional` to not be broken, it *must* check this possibility. – Nicol Bolas Mar 27 '17 at 14:16
  • @NicolBolas: The whole point of the question is that we could, perhaps, stop using nullptr, since with `std::optional` being in the language, it can be decided it is no longer used. Of course I'm talking about new code, not old code, C APIs etc. – einpoklum Mar 27 '17 at 14:20
  • @einpoklum how about [`not_null`](http://stackoverflow.com/questions/33306553/gslnot-nullt-vs-stdreference-wrappert-vs-t)? – Quentin Mar 27 '17 at 14:27
  • @Quentin: We will no longer need it. It's redundant. There will only be valid pointers. The concept of a null pointer can be deprecated (again, ignoring existing code). – einpoklum Mar 27 '17 at 14:38
  • 2
    @einpoklum: Who would decide that? How would it be decided? What about all of the APIs that exist in the standard presently that deal with NULL pointers? Not to mention all of the APIs that exist in other libraries. It's absurd to even consider that `optional` could somehow replace NULL pointers. – Nicol Bolas Mar 27 '17 at 14:41
  • @NicolBolas: So, yours is a valid argument against: "Too much legacy and out-of-language conventions to do this". You can make that an answer. – einpoklum Mar 27 '17 at 14:43
  • Almost all pointer values are not valid pointers. Consider: `auto stream_ptr = reinterpret_cast(0xDEADBEEF); int i; (*stream_ptr) >> i;` Unless you know that *somebody* initialised an `istream` there, you've got UB – Caleth Mar 27 '17 at 15:28
  • @Caleth: That's a perfectly valid pointer value - it's the memroy address`0xDEADBEEF`. The pointed-to memory may not be valid; or the memory region may not have been allocated, but that's not a problem with the pointer. In other words - if we were to write a function which takes a pointer, and you got `0xDEADBEEF`, there would be nothing you can do (inherent to the language, or the standard library) to check whether it's valid except dereference it. – einpoklum Mar 27 '17 at 15:58
  • @einpoklum it is a perfectly valid `void*`, but it is not a valid `T*` **for all T** – Caleth Mar 27 '17 at 16:17
  • @Caleth: Granted, but there's nothing special about `0x0` in this respect. Worrying about a pointer to an unconstructed T is a concern now and will be a concern in the same way if we deprecated the use of nullptr and used `std::optional` instead. – einpoklum Mar 27 '17 at 16:33
  • @einpoklum the point is that the appropriate replacement for `T*` in the scenario "We might have a `T` for you" is `optional`, not `optional` – Caleth Mar 27 '17 at 16:38
  • @Caleth: So you're saying that perhaps we could forego the use of non-void pointers altogether? Hmm. – einpoklum Mar 27 '17 at 16:48
  • An 'always valid pointer' (assuming no complications like ownership) is, to me, a reference. So, if you want to put it in a container, use `std::reference_wrapper`. There: no default constructibility with an undefined value, no `null` state, no accidental arithmetic, etc. Then if it's optional, use `std::optional< std::reference_wrapper >`. *Or*, if you want the ability to maintain a `null` state but don't want that other stuff I mentioned, use the proposed `observer_ptr`. – underscore_d Sep 30 '18 at 15:16
  • @underscore_d: A reference isn't mutable, and you don't do arithmetic with it. – einpoklum Sep 30 '18 at 16:14

3 Answers3

2

Pointer embed the optional semantic already. Therefore std::optional<T*> would be redundant to say the least and the introduction of said feature won't impact much raw pointers.

This is also true with std::unique_ptr, std::shared_ptr and std::weak_ptr all of which can also be nullptr.

Just like with any new feature we have to ponder what's pragmatic versus our innate urge to use it everywhere it's feasible.

Shoe
  • 74,840
  • 36
  • 166
  • 272
  • A pointer doesn't embed the optional semantic already; we've arbitrarily "sacrificing" one of the possible addresses in memory and interpreting it as a poor man's `nullopt`. Now, we no longer have to do that. – einpoklum Mar 27 '17 at 14:40
  • 2
    @einpoklum: "*A pointer doesn't embed the optional semantic already*" How is checking against `nullptr` any different from checking against `nullopt`? Pointers very much do "embed the optional semantic". – Nicol Bolas Mar 27 '17 at 14:43
  • 2
    @NicolBolas: Oh, a _big_ difference : (1) No arbitrary if'ing, just case class decomposition (although we don't have syntax for that right now). (2) Uniformity in the use of optional semantic for all types (3) No longer sacrificing any address in the address space, so we can safely use address 0x0 . Granted (3) is a minor point, but (1) is big language-design-wise IMHO. – einpoklum Mar 27 '17 at 14:51
  • 1
    @einpoklum: "*Uniformity in the use of optional semantic for all types*" Which can easily be achieved by making a global template function to handle such tests for both `optional` and `T*`. Or any other types that a user might want to add. "*No longer sacrificing any address in the address space, so we can safely use address 0x0 .*" The null pointer constant does not have to have the value zero. Furthermore, just because C++ isn't using NULL pointers doesn't mean the compiler won't. After all, C++ still has to link with C code. – Nicol Bolas Mar 27 '17 at 14:57
1

As I think my conversation in comments is not sufficient I provide answer to einpoklum comment. This answer is more like suplement to Nir Friedman answer.

Please read http://en.cppreference.com/w/cpp/language/pointer. Pointer is a type. It's have state describe as Null pointer. Pointer is not a CPU's memory address (but it could be implemented like that), it is abstract type that instance point to another object. Most of pointers has relations with corresponding type but not all of them as you can point by void pointer to any object.

Community
  • 1
  • 1
Logman
  • 4,031
  • 1
  • 23
  • 35
0

Because all pointer types can be null, this unfortunately doesn't make sense in C++. You would need people to accept, by convention, that:

unique_ptr<Foo> make_foo();

Never returns a null pointer, even though it can. But then you'd wonder, if the author actually followed the convention, so you'd look at the docs anyway. And if this function never returns nullptr, then the docs should say it anyhow. And if it can, the docs should say it also.

Basically, the idea behind optional is to use the type system to protect people. Particularly to encourage people to write types that are constructed into a valid state immediately. In other words, people often used to write types that were effectively OptionalFoo, because they weren't always in a valid state. So instead, encourage people to write Foo, and use optional<Foo> when they need it. That way, the type Foo is always valid.

However, pointers, raw and smart, just cannot be guaranteed to be in a valid state. Every pointer is already an optional pointer. We can argue over the exact meaning of validity here, but basically you have a precondition on calling * on all pointers. The whole point of optional is to remove preconditions; you don't need to call f.is_valid() because you wrote a Foo instead of an OptionalFoo.

By the way, the reason why pointers cannot make this guarantee in C++ is actually rooted in the absence of destructive move in C++. If you try to write a not_null_unique_ptr, what would the move constructor look like?

Nir Friedman
  • 17,108
  • 2
  • 44
  • 72
  • Nir, `make_unique()` _always_ returns a valid pointer (wrapped in an `std::unique_ptr`. So if you write a `make_foo()`, you should be doing the same even today - throwing `std::bad_alloc()` on failure. – einpoklum Mar 27 '17 at 14:45
  • 3
    @einpoklum *If* it returns, then yes, it does. But that is covered by documentation, it's not enforced by the type system in any way. I'm starting to get the sense from your response to the answers posted (both of which are pretty reasonable) that your "question" is really you trying to argue that we should do a certain thing. – Nir Friedman Mar 27 '17 at 14:45
  • The "nullness" of pointers is not represented in the type system. There are no null types, or a null pointer type, there's just "pointer". And nullness is not inherent in it, it's artificial. Absolutely no reason why I can't use any address in the address space (well, other than not having enough memory of course). – einpoklum Mar 27 '17 at 14:47
  • 3
    @einpoklum No, that is not true. Pointers have a well defined hole in the type system (sadly). Dereferencing a null pointer is *not* just dereferencing an address that probably has garbage. It is actually defined as undefined behavior. You may not like that pointers encode optionality, or wish they didn't, but the standard officially sanctions their optionality. – Nir Friedman Mar 27 '17 at 14:49
  • Now _that_ is an answer to my question (as opposed to what you've written above). If you can elaborate in your answer how pointers are treated differently in the type system that would be great. Note that if it's just undefined behavior for `*((void *) 0x0)`, that could become defined in the next standard. – einpoklum Mar 27 '17 at 14:53
  • 1
    @einpoklum I was taking it as a given that dereferencing a null pointer is a null pointer is UB. I feel like you probably knew that already, so I didn't get into it. My last few sentences already explains pretty well why it cannot just "become defined" in the next standard. You simply cannot have good smart pointers without destructive move or optionality of some kind. – Nir Friedman Mar 27 '17 at 14:56
  • @einpoklum you are thinking of pointers in integer categories and that's just wrong. Pointers can have invalid values that you cannot use to allocate memory and it's not important if it's one value (ex. 0) or multiple of them. Think of float that have multiple NaN values and no one is arguing about not using whole 32bit space of values to represent correct number. – Logman Mar 27 '17 at 15:03
  • @Logman: That's not inherent in the type; it's a platform restriction (or maybe even a runtime restriction). If I have an embedded platform with 4GB of byte-addressable memory, why isn't every address a valid value for a pointer? – einpoklum Mar 27 '17 at 15:07
  • 1
    @einpoklum because **almost all** of the objects that would be pointed to by those pointers haven't been constructed. None of the invariants of the types of those objects would hold. Well done, you've just blown a **massive hole** in the capability to reason about programs. – Caleth Mar 27 '17 at 15:21
  • 1
    @einpoklum ok then let me explain something. In computer word you could be restricted to some boundaries. In ex. right now you actually can't address whole memory as you are restricted to addressing 8 bit words (on x86). If you want to address 4GB of data you need bigger pointers. This is just how it's works. What about addressing 5GB of data etc. there always will be restrictions as that how discrete not infinite machines works. Pointer to type is just another type and instance of it has it's own states and transactions. – Logman Mar 27 '17 at 15:22
  • @Caleth: See my reply to your similar comment above. No hole is blown, the same hole exists now except that you special-case address `0x0` by checking for nullptr. – einpoklum Mar 27 '17 at 16:01
  • @Logman: It's not "just how it works". And, again, this has nothing to do with the type, these are platform restrictions which the language is unconcerned with even now. – einpoklum Mar 27 '17 at 16:04