Hacker News new | ask | show | jobs
by Animats 38 days ago
In C++, that distinction supposedly exists. References should never be null, while pointers can be. But there's no enforcement.

    int& ref = *ptr;
ought to generate a panic for a null pointer. But it doesn't. They were so close to getting it right.
5 comments

There's nothing particularly special about null pointers: you can also have an invalid non-null pointer, e.g. through pointer arithmetic.

When you write `int& ref = *ptr;` you are dereferencing the pointer with `*ptr` therefore you have promised that it's valid. The compiler doesn't need to do anything to validate ptr because it already has your assurance.

It's really no different than if you were to write `printf("%d", *ptr);`. It's only a little weird because in `ref = *ptr;` the compiler doesn't actually emit any instruction for the dereference, but that doesn't mean that the assurance you gave doesn't exist.

It would indeed be problematic were it to be `int& ref = ptr;` without the dereference but it's not.

> When you write `int& ref = ptr;` you are dereferencing the pointer with `ptr` therefore you have promised that it's valid.

Yes, that's exactly the problem. "I promise I didn't make a mistake when reasoning about this code" is tied for worst strategy in the world for preventing bugs, along with every other strategy that doesn't actually prevent bugs.

The danger and utility of pointer are two sides of the same coin. You can reduce the need for pointers but not eliminate them completely if you want to be able to call C functions or build low-level data structures.

The real problem is that references are not good enough in C++, so some C++ developers end up using pointers for everything. Rust's references are good enough that you can avoid using pointers most of the time.

For the longest time I thought this line would lead to a crash just because it seemed so obvious. So close indeed.
Im not entirely sure this helps with your point but;

The contract is that the reference is still non-null, and that the error is dereferencing the pointer. There’s two big problems with defining the behaviour of the deterrence - 0 is a valid memory address on some (ancient) platforms so for better or worse the behaviour is platform dependent.

The other is that there’s many other ways to have absolute garbage in a pointer that aren’t null.

    int& foo() { 
        int local = 42;
        return local;
    }
Now, a compiler catches this case, but the point is that null isn’t the only invalid state that needs to be checked. Adding a compiler overhead of checking each pointer to every single pointer dereference wouldn’t work.

Modern codebases ran with static analysis tools will catch these errors (honestly even valgrind will find most if not all of these).

> They were so close to getting it right.

The philosophy of C++ is to not introduce unnecessary overhead, and to trust the programmer. This design choice is prevalent throughout the language. They were never going to make an exception, especially for something as prevalently used as references.

There are countless examples of this "no unnecessary overhead and/or trust the programmer" choice:

- primitive types and standard containers are not thread safe - it's up to the programmer to know this and use them accordingly.

- std::unique_ptr lets you grab the underlying raw pointer, in which case it's no longer a "unique_ptr". But there are cases in which it's useful to do this (e.g. interfacing with C code), so they let you do it, and trust that you do it in a safe way. They could have made unique_ptr not support this, but then it would be less useful (or force you into copying data unnecessarily to call an API that requires a raw pointer).

> But there's no enforcement.

There's no strict enforcement, but it is undefined behaviour, so compilers can randomly choose to act as if it's enforced and simply crash your program or make it act weirdly.

> primitive types and standard containers are not thread safe - it's up to the programmer to know this and use them accordingly.

Which (sort of) makes sense: most types should not be used across threads. Having everything use atomics/mutexes under the hood would have significant overhead. However, the problem is that the language doesn't then protect you against using these across threads by mistake, this is one of the things that I really like about Rust.

Funnily enough, shared_ptr in C++ is thread safe (for the reference count at least), leading to pointless overhead when not used between threads. Rust has both thread safe and non-thread safe versions (Arc and Rc respectively), and it will error if you try to send an Rc to another thread.

mutable XOR alias is also key here

For a typical type Goose it's fine if two threads can both look at the Goose (via a reference, pointer or whatever), so long as nobody can mutate the Goose. If thread A finds that the Goose is Happy, thread B weighs the Goose and finds it to be Heavy, and thread A again measures the length of the Goose as 860 millimetres this is all fine, it won't matter if (by the vagaries of hardware) the weight is measured before the length or after, there's no difference.

In Rust this is reflected in &Goose, the immutable reference to a Goose, being Send, ie a thing you can give to other threads. The mutable reference &mut Goose is not Send.

> The philosophy of C++ is to not introduce unnecessary overhead, and to trust the programmer.

The first part of that is reasonable. The second part is just naive; nobody writes bug-free code, so if your strategy for not having memory corruption is "just don't screw it up", you're going to get quite a lot of memory corruption, and that's how we ended up where we are today.

No, it ought to generate a compilation error unless the compiler can prove that the pointer isn't null.

... but that only works if you design properly from day one.