Rfcs: Default is not implemented for raw pointers (*const and *mut)

Created on 8 Jun 2018  Â·  20Comments  Â·  Source: rust-lang/rfcs

There is little info about this anywhere but is this an oversight?

A default value that makes the pointers point nowhere (like NULL) looks ok to me, but maybe I am missing something.

T-libs

Most helpful comment

For some reason I was under the impression this was deliberately not implemented, since NULL isn't the safest thing in the world. Though I don't think I've seen an explicit discussion about that.

All 20 comments

For some reason I was under the impression this was deliberately not implemented, since NULL isn't the safest thing in the world. Though I don't think I've seen an explicit discussion about that.

Right, I agree that null feels better when it’s a explicit choice rather than a possibly-derived possibly-not-thought-much-about default.

There are many better options than raw pointers for code that wants to handle null explicitly, or not at all, (NonNull, &T, Option<&T>, Option<NonNull>, ...).

The whole point of raw pointers is that they can be null, and ptr::is_null is one of the most used raw pointer methods because of this. The only code I can see implementing Default for raw pointers making more dangerous is code that was already broken to begin with.

Honestly, working with raw pointers is always hard, but the only thing adding hoops does for me is requiring me to spend time and effort coming up with workarounds for no good reason.

Personally, I've used raw pointers mostly in situations where they can't be null. I suppose I could have theoretically used NonNull instead, but most existing APIs use raw pointers – including the pointer arithmetic methods in std::ptr, the as_ptr/from_raw_parts/etc. family, bindgen, and so on.

The whole point of raw pointers is that they can be null

It‘s really not. Your statement matches not *const T but Option<&T>. The defining feature of raw pointers is that the programmer assumes the responsibility for plenty of safety guarantees, including (but not limited to) nullability (which may even be contextual).

The whole point of raw pointers is that they can be null

I think this is only (mostly) true in C++, where the various smart pointers introduced in modern versions of the language have retroactively made the more fundamental raw pointers only the best choice when either a) nullability is required or b) the pointer is completely non-owning.

For a), I believe that's mainly because std::optional<std::unique_ptr> is not only verbose but not quite logically equivalent to a nullable raw pointer the way Option<&T> is in Rust. Unfortunately, std::unique_ptr can be "empty" (I think this is mainly because of how move constructors work? not sure), so it's not strictly non-nullable, and thus std::optional<std::unique_ptr> would end up having "two different nulls" which leads to the same sort of weirdness one sees with Java's retroactively-introduced Optional type.

For b), that's just because std::observer_ptr is not standard yet.

@main--

The whole point of raw pointers is that they can be null

It‘s really not. Your statement matches not *const T but Option<&T>. The defining feature of raw pointers is that the programmer assumes the responsibility for plenty of safety guarantees,

These "features" apply to other pointer types like NonNull. The only Rust types that can represent a pointer to null are raw pointers.


@Ixrec

I believe that's mainly because std::optional is not only verbose but not quite logically equivalent to a nullable raw pointer the way Option<&T>

Option<&T> == None is an Option that does not contain a &T, which is semantically very different from a raw pointer to null, which is still a pointer. One way to think about this is consider what would happen if we were to disable the non-zero optimization in Option. Semantically all safe code would still work the same, but these two types would not be layout compatible anymore.


@comex

Personally, I've used raw pointers mostly in situations where they can't be null. I suppose I could have theoretically used NonNull instead, but most existing APIs use raw pointers – including the pointer arithmetic methods in std::ptr, the as_ptr/from_raw_parts/etc. family, bindgen, and so on.

I feel your pain and I think this is an issue worth solving, but the solution is not to make raw pointers unnecessarily harder to use, but rather to make NonNull easier to use. If NonNull would auto deref/coerce to a raw pointer most of the existing frictions would disappear.

These "features" apply to other pointer types like NonNull. The only Rust types that can represent a pointer to null are raw pointers.

What about usize? [u8; 8]? Option<&T>? You disqualify that last one on the grounds of it not having a guaranteed memory representation (it actually does, as far as I remember?) while also excluding anything that isn't a canonical pointer type (*const T, NonNull). This position doesn't make sense to me.

Unless you specifically care about layout optimizations, there is no reason to use NonNull over a raw pointer. Unlike the typesafe equivalents, it offers absolutely no ergonomics or safety benefits - on the contrary, it even introduces additional undefined behavior. In other words: you don't use *const T because the pointer is nullable, you use it because you want any pointer (nullable or not). Now if you figure out that this pointer can never be null and if you need the layout optimization, only then would you even consider using NonNull.

What about usize? [u8; 8]? Option<&T>? You disqualify that last one on the grounds of it not having a guaranteed memory representation

I disqualified these on the grounds that they are not pointers that can point to the null address and that you can dereference. Just because something is layout compatible with a raw pointer does not make it a raw pointer.

Now if you figure out that this pointer can never be null and if you need the layout optimization, only then would you even consider using NonNull.

I disagree. If you figure that this pointer can never be null you should make it NonNull to more clearly express your intent independently of whether you want a null pointer optimization or not.

I don't understand the claim that Option<&T> is not a pointer while nullable raw pointers are. An Option<&T> is either the address of a T value, or None. A raw pointer to T is either the address of a T, or null. An Option<&T> is obviously not a _raw_ pointer, but it seems to be a perfectly adequate non-raw pointer to me. It seems to do exactly as much pointing as any raw pointer, or a "smart pointer" like Arc or Box.

But maybe that's the wrong question entirely. I think we're only playing the "what is a pointer?" game because some of us didn't understand your earlier claim that:

The whole point of raw pointers is that they can be null

Regardless of how we differ in our usage of words like "pointer" or "null", I'm pretty sure that there are non-raw pointer/reference types in Rust which have a special value that means not pointing/referring to anything, and that Option<&T> is such a type. So given that, what is special about raw pointers other than unsafety? Why would someone prefer to represent nullability with ptr::null values rather than None values, assuming unsafety was not required?

pointers that can point to the null address and that you can dereference

Are you trying to say that you actually want to dereference the null value, and that's what makes raw pointers the type of choice? I thought doing that was undefined behavior.

I don't understand the claim that Option<&T> is not a pointer while nullable raw pointers are. An Option<&T> is either the address of a T value, or None. A raw pointer to T is either the address of a T, or null.

The difference is that None is the lack of an address while null is an address. You can try to do pointer arithmetic with null, you can't with None because it is not an address.

The point I was trying to make is that the only types in Rust that let you store this null address as a value that you can manipulate are raw pointers.

I'm pretty sure that there are non-raw pointer/reference types in Rust which have a special value that means not pointing/referring to anything

A raw pointer that points to the address null does not point to nothing, it points to an object of some type on the address null. This is why Option<*const T> does not apply the "null pointer optimization", there is a difference between having no pointer, and having a pointer pointing to null.

Are you trying to say that you actually want to dereference the null value, and that's what makes raw pointers the type of choice? I thought doing that was undefined behavior.

No, what I said is that these types cannot represent the null address as a value.

In any case, the only argument I've actually heard against improving the raw pointer ergonomics by implementing traits like Default is that it might make it more tricky for those who are using raw pointers without handling null correctly.

It was pointed out that this happens if users expect for raw pointers to never be null, to which I've argued that such cases are bugs because either the user didn't understood that the pointer could be null, or because the user didn't use the appropriate type for their pointer (like, for example, NonNull).

@comex raised a very good point about the bad ergonomics of interfacing these types with raw pointer APIs, but while that's a problem worth solving, it's orthogonal to this one.

So I am very unconvinced by the counter argument because at least for the reasons raised, the only situation in which Default can be dangerous are situations that are already dangerous or silent because either the code already has a bug, or because the user didn't use the proper pointer type.

AtomicPtr::default() already returns null btw. So the standard library is at best inconsistent right now.

I think Default should convey meaningful information. Having a default pointer is a bit akin to have a default age, to me. It’s possible, yes. Does it make sense? I don’t think so.

I would be more interested to have a PtrArithmetic trait that would have a const NULL: Self associated type. That would make much more sense to me.

@phaazon how is having a default value for pointers different than having a default value for Option ?

It’s not, and to me, Option shouldn’t have a Default value. Depending on the usecase, the default value could be None or Some(T::default()).

Ah I see, yes that is fair. I suppose that's just like the Monoid instance for Maybe in Haskell, there are multiple ways to define that. The question is whether it is a better trade-off to provide a default one or not. I think that providing a default is better, and in the cases where the default doesn't fit, one can always write a newtype - I personally never have to do so for Option and I can imagine that this can be painful, but that's a problem that can be solved.

For raw pointers the situation is a bit different, and I don't know of any identity element beyond NULL that might make sense (EDIT: at least for the offset(ptr0, ptr1) operation, NULL is the only value that makes sense to me).

I would be more interested to have a PtrArithmetic trait that would have a const NULL: Self associated type. That would make much more sense to me.

Do you have any applications in mind in which you would assign PtrArithmetic::NULL a different value than NULL ?

Ah I see, yes that is fair. I suppose that's just like the Monoid instance for Maybe in Haskell, there are multiple ways to define that.

Yes, but Monoid has laws whereas Default has none.

I think that providing a default is better.

Why? No default is better than a bad/wrong default.

Do you have any applications in mind in which you would assign PtrArithmetic::NULL a different value than NULL ?

No, NULL = NULL seems like a fine assertion to me. But being able to be polymorphic on the type of pointers and _null pointers_ seem like something I could use in FFI code, for instance. E.g. a value of type T: PtrArithmetic can be set to T::NULL.

Notice that Default does not promise any kind of "right" default, it just promises that it will give you "a value" (I'm not sure from reading its docs that this value even has always to be the same), and with that you can use derive(Default) and Foo { x: 42, ..Default::default() }; on types containing your type to initialize them to some default value, and that's it.

That's a useful and valuable feature, and it doesn't really matter what the value by Default::default returned is because Default doesn't have laws and it does not guarantee anything about the value returned.

This does not prevent particular implementations of Default from guaranteeing more, e.g., <i32 as Default>::default() can guarantee that it returns 0_i32, and that's fine, and happens to be the identity element, but generic code fn foo<T: Default>() doesn't get these extra guarantees.

From this POV, Option::None is a good default value for Option<T> because it is the only value that can be provided even if T does not implement default. And for pointers, NULL is a useful value as well because it does not require allocating memory, and can be easily checked with ptr::is_null to make sure such pointers aren't dereferenced.

Also, these values do not prevent people from adding others. You can implement a Monoid trait for Option and raw pointers and give them an identity element that can be used as a different default if you wanted, and couple that trait with laws, but that's not what Default does.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mahkoh picture mahkoh  Â·  3Comments

silversolver1 picture silversolver1  Â·  3Comments

p-avital picture p-avital  Â·  3Comments

marinintim picture marinintim  Â·  3Comments

Diggsey picture Diggsey  Â·  3Comments