Skip to content

Default is not implemented for raw pointers (*const and *mut) #2464

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
gnzlbg opened this issue Jun 8, 2018 · 28 comments · Fixed by rust-lang/rust#139535
Closed

Default is not implemented for raw pointers (*const and *mut) #2464

gnzlbg opened this issue Jun 8, 2018 · 28 comments · Fixed by rust-lang/rust#139535
Labels
T-libs-api Relevant to the library API team, which will review and decide on the RFC.

Comments

@gnzlbg
Copy link
Contributor

gnzlbg commented Jun 8, 2018

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.

@Ixrec
Copy link
Contributor

Ixrec commented Jun 8, 2018

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.

@SimonSapin
Copy link
Contributor

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

@gnzlbg
Copy link
Contributor Author

gnzlbg commented Jun 8, 2018

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.

@comex
Copy link

comex commented Jun 8, 2018

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.

@main--
Copy link

main-- commented Jun 10, 2018

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

@Ixrec
Copy link
Contributor

Ixrec commented Jun 10, 2018

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.

@gnzlbg
Copy link
Contributor Author

gnzlbg commented Jun 13, 2018

@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::optionalstd::unique_ptr 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.

@main--
Copy link

main-- commented Jun 13, 2018

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.

@gnzlbg
Copy link
Contributor Author

gnzlbg commented Jun 13, 2018

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.

@Ixrec
Copy link
Contributor

Ixrec commented Jun 13, 2018

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.

@gnzlbg
Copy link
Contributor Author

gnzlbg commented Jun 13, 2018

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.

@gnzlbg
Copy link
Contributor Author

gnzlbg commented Jun 13, 2018

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.

@Centril Centril added the T-libs-api Relevant to the library API team, which will review and decide on the RFC. label Oct 14, 2018
@gnzlbg
Copy link
Contributor Author

gnzlbg commented Nov 8, 2019

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

@hadronized
Copy link
Contributor

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.

@gnzlbg
Copy link
Contributor Author

gnzlbg commented Nov 8, 2019

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

@hadronized
Copy link
Contributor

hadronized commented Nov 8, 2019

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()).

@gnzlbg
Copy link
Contributor Author

gnzlbg commented Nov 8, 2019

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 ?

@hadronized
Copy link
Contributor

hadronized commented Nov 8, 2019

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.

@gnzlbg
Copy link
Contributor Author

gnzlbg commented Nov 8, 2019

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.

@Morglod
Copy link

Morglod commented Jun 27, 2021

(sarcasm) Absolutely safe to make mut pointer from const reference:

fn foo(x: &i32) -> *mut i32 {
    return ((&(*x)) as *const i32) as *mut i32;
}

fn main() {
    let mut kekw: *mut i32 = unsafe { 0 as *mut i32 };

    {
        let mut x = 1337;
        let ref_x: &i32 = &x;

        kekw = foo(&ref_x);
    }
}

but its unsafe to make raw pointer zeroed with default trait

the only way some one could 'shoot his leg' with zeroed pointer is:

pointer = 0;
unsafe { *pointer }

which may happen - never

but every time I want to use raw pointer in struct, I should do some strange mess with it
mmm

just give me "std::ptr::SuperUnsafePtr" with zero by default;
and disallow to publish crates with this type used

I will decide what to do in my project by myself with this SuperUnsafePtr


PS For everyone with same question 🤓

Added crate for macro: rust_var_zeroed

With struct wrapping:

pub struct ZeroedMutPtr<T> (pub *mut T);

impl<T> Default for ZeroedMutPtr<T> {
    fn default() -> ZeroedMutPtr<T> {
        return unsafe {
            ZeroedMutPtr (0 as *mut T)
        };
    }
}

impl<T> ZeroedMutPtr<T> {
    fn to_ref(&self) -> &T {
        unsafe { &*(self.0) }
    }

    fn to_mut_ref(&self) -> &T {
        unsafe { &*(self.0) }
    }
}

With macro (C way):

#[macro_export]
macro_rules! var_stack_zeroed {
    ($var_name: ident, $var_type: ty) => {
        let mut $var_name: &mut $var_type;
        unsafe {
            $var_name = &mut *(((&mut ([ 0 as u8; std::mem::size_of::<$var_type>() ])) as *mut [u8; std::mem::size_of::<$var_type>()]) as *mut $var_type)
        }
    };
}

#[macro_export]
macro_rules! var_heap_zeroed {
    ($var_name: ident, $var_type: ty) => {
        let mut $var_name: *mut $var_type;
        unsafe {
            let layout = std::alloc::Layout::new::<$var_type>();
            $var_name = std::alloc::alloc_zeroed(layout) as *mut $var_type;
        }
    };
}

struct MyStruct {
    a: i32,
    ptr: *mut MyStruct,
}

fn main() {
    var_stack_zeroed!(my_var, MyStruct);
    println!("{:?}", my_var.ptr); // -> 0x0

    var_heap_zeroed!(ptr_str, MyStruct);
    println!("{}", unsafe { (*ptr_str).a }); // -> 0
}

@Lokathor
Copy link
Contributor

the expression 0 as *mut i32 is not unsafe, so I'm unclear what you're saying here.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=285e72e32b9d304502778e0836204f9d

@LegionMammal978
Copy link
Contributor

LegionMammal978 commented Dec 7, 2022

I've run into the lack of Default for pointers while writing native Java functions with jni-sys. In particular, when a native Java function throws an exception, it generally returns the zero or null value for the return type, then the caller uses ExceptionCheck() to determine that an exception occurred. Based on this, I attempted to write a generic wrapper for native functions, which throws an exception and returns Default::default() if the function body panics. However, this does not work for functions returning jobject, which is a type alias for *mut _jobject.

Many C interfaces use the null pointer to denote the equivalent of Option::None. In this instance, I could have replaced the jobject return type with Option<NonNull<_jobject>>, since the function pointer is cast to *mut c_void regardless. But in the general case, this might not always be possible: automatic binding generators can force the signature of a function to accept or return a possibly-null raw pointer, where the null value indicates None.

I just don't understand how implementing Default for (thin) raw pointers could cause any additional footguns. A raw pointer by itself has no invariants. If you want any guarantees at all about what can be done with the pointer, then you must either carefully keep it inside a controlled unsafe context, or store it in a data structure that maintains those particular invariants.

In particular, it takes several invariants just to dereference a pointer at all: it must be non-null, writable if you're trying to write to it, not aliased with unique references, not concurrently accessed in a way that would cause a data race, and not already deallocated. If you're ever in a situation where you're trying to dereference a Default::default() pointer, then you've already likely made a number of mistakes along the way.

Thus, since null raw pointers have a number of valid use cases in FFI code (to safely construct zeroed objects to be filled, or to indicate the equivalent of None to a C API), and since a Default impl likely can't create any footguns that weren't already present, I simply don't see why such an impl shouldn't exist.

@ajeetdsouza
Copy link

ajeetdsouza commented Apr 23, 2023

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.

@phaazon I don't agree with this take. By this logic, the default value of a u64 should not be 0; yet it is. The default value of a bool should not be false; yet it is.

Default is always a contextual thing, it rarely works for everyone. Instead, the choice that appears to have been made in the standard library is for Default to set everything to its "zero" value:

  • Option -> None
  • Vec / String -> empty
  • u64 -> 0
  • bool -> false

In keeping with this theme, it makes sense for a raw pointer to be default initialized as 0 (aka NULL). I would attribute the missing default implementation to incompleteness of the standard library, rather than a safety measure.

Additionally, default initializing any value incorrectly can be disastrous. Think of what might happen here:

#[derive(Default)]
struct Robot {
  enable_ethics: bool, // defaults to `false`!!!
}

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

This is an excellent point.

@hadronized
Copy link
Contributor

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.

@phaazon I don't agree with this take. By this logic, the default value of a u64 should not be 0; yet it is. The default value of a bool should not be false; yet it is.

Default is always a contextual thing, it rarely works for everyone. Instead, the choice that appears to have been made in the standard library is for Default to set everything to its "zero" value:

  • Option -> None
  • Vec / String -> empty
  • u64 -> 0
  • bool -> false

In keeping with this theme, it makes sense for a raw pointer to be default initialized as 0 (aka NULL). I would attribute the missing default implementation to incompleteness of the standard library, rather than a safety measure.

Additionally, default initializing any value incorrectly can be disastrous. Think of what might happen here:

#[derive(Default)]
struct Robot {
  enable_ethics: bool, // defaults to `false`!!!
}

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

This is an excellent point.

Well, yeah, having a « zero » value is useful… but it’s never useful on its own — i.e. Monoid has an empty value because we reason about that value with a given binary function. In linear algebra crates, we often want to have a zero value, and most of the time there is a trait for that, Zero, and even a trait for the « 1-value », One. Why? Because it makes sense to have the « default » value for addition and the default value for multiplication.

In the case of a default value for an option, I think it should be fine to have another trait, like Empty, Absent, whatever. Because you have to check what the default value is, I rarely use Default because I don’t find it useful at all, either in specific code, where I would directly use ::new(), or in generic code, which cannot fully reason about what a default value is.

@LegionMammal978
Copy link
Contributor

LegionMammal978 commented May 30, 2023

In the case of a default value for an option, I think it should be fine to have another trait, like Empty, Absent, whatever. Because you have to check what the default value is, I rarely use Default because I don’t find it useful at all, either in specific code, where I would directly use ::new(), or in generic code, which cannot fully reason about what a default value is.

The point of the current Default trait is that it's anything that makes sense as an initial or fallback value, not necessarily some mathematically pure identity value. For container types like Option<T> or Vec<T> which can hold zero or more elements, the empty container naturally fits this criterion; there is nothing in it initially, and then you can add something into it.

Likewise, the most important property of the null pointer isn't that its integer value is zero, but that it does not point to any value. Every other pointer can possibly point to something. That's why so many C APIs use a null pointer to indicate a default value, because if the user specifies a null pointer, there is 0 chance that they were specifying an actual value to be read from or written to.

You can have your own gripes about the Default trait, but in the context of the trait as it exists today, "a pointer that points to nothing" is a natural logical extension of an empty container, which makes sense to use as an initial value. The naming of core::mem::take() reinforces this: you take all the elements the argument used to contain, leaving behind an empty container. Similarly, if you take from a pointer that points to some value, then it would make sense that you'd leave behind a pointer not pointing to any value.

@kennytm
Copy link
Member

kennytm commented Apr 9, 2025

This request has been submitted as an ACP rust-lang/libs-team#571 and has been accepted.

The implementation rust-lang/rust#139535 is currently waiting for FCP to finish. It will be insta-stable on v1.88 if no concern.

@ChrisDenton
Copy link
Member

I'm going to close this in favour of the ACP/FCP. An issue on the RFC repo is kind of an odd place for libs discussions in any case. I for one wasn't initially aware of it.

jhpratt added a commit to jhpratt/rust that referenced this issue Apr 19, 2025
Implement `Default` for raw pointers

ACP: rust-lang/libs-team#571

This is instantly stable so we will need an FCP here.

Closes rust-lang/rfcs#2464
ChrisDenton added a commit to ChrisDenton/rust that referenced this issue Apr 19, 2025
Implement `Default` for raw pointers

ACP: rust-lang/libs-team#571

This is instantly stable so we will need an FCP here.

Closes rust-lang/rfcs#2464
rust-timer added a commit to rust-lang-ci/rust that referenced this issue Apr 19, 2025
Rollup merge of rust-lang#139535 - ChrisDenton:default-ptr, r=tgross35

Implement `Default` for raw pointers

ACP: rust-lang/libs-team#571

This is instantly stable so we will need an FCP here.

Closes rust-lang/rfcs#2464
github-actions bot pushed a commit to model-checking/verify-rust-std that referenced this issue Apr 22, 2025
Implement `Default` for raw pointers

ACP: rust-lang/libs-team#571

This is instantly stable so we will need an FCP here.

Closes rust-lang/rfcs#2464
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-libs-api Relevant to the library API team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

15 participants