Hacker News new | past | comments | ask | show | jobs | submit login

Doing it requires two threads.

Thread A sets a shared reference to a newly allocated and null initialized reference to Foo:

   shared = new Foo();
While that's running, thread B invokes a method on the reference that assumes bar is non-null:

   shared.useBar();  // null pointer exception
Later, thread A runs the constructor for Foo.



I think you're right, if access to shared is not in any way synchronized. But the correct way to handle this, at least in this case, is to mark shared as volatile, which guarantees thread B will only ever read null or a fully constructed Foo from shared. This has been the case since Java 5, released 20 years ago, thanks to JSR-133.


By that standard, C and C++ are much worse, since they offer no runtime encapsulation at all, and have much worse and more subtle multithreaded errors (e.g. Java at least guarantees that all native word sized reads/writes are atomic, if I recall correctly). C++ doesn't even guarantee that a reference can't be null, or worse, deallocated before it is dereferenced. They allow you to specify that a field is of some type and shouldn't be null, which is nice, but they don't enforce that in any way, they just call any code path that violates it UB.

For example, this is code that any C or C++ compiler will happily run and do something:

  struct Bar {
    int b;
  };
  struct Foo {
    struct Bar bar;
  } foo;

  strcpy((char*)(&foo), "ABC");
Or in relation to null C++ references:

  int& foo(int* p) {
    return *p;
  } 
  
  int &r = foo(nullptr); //UB, but in practice will likely result in a null reference at runtime
Similarly, accessing an object from multiple threads without synchronization means its value is not fully defined in Java. Unlike C or C++, it is at least known to be a Java type, not a memory corruption vulnerability.


We can quibble on definitions here, but a reference in C++ can not be null. The undefined behavior happens before any assignment to a reference is executed so that at the moment that the assignment happens, the reference is guaranteed to not be null.

In your example, it's the dereference of the pointer to p that is undefined behavior, so anything that happens after that point is also undefined behavior. Note that means there is never an actual assignment to r if p is null.

As I mentioned earlier, this might seem like quibbling with definitions, but this is the proper mental model to have with respect to C++'s semantics.

Having said that, I don't disagree with the main crux of your point, which is that C++'s semantics are terrible and there is little that the language provides to write correct code, but I do think there are subtleties on this matter that are worth clarifying.


I agree, but by that same definition, a private reference field in Java that is initialized in the constructor also can't be null.

My point is that we can compare two things: valid programs, or programs that compile.

In valid C++ programs, references can't be null and there are no data races. In valid Java programs, all final fields initialized in an object's constructor have that value for the lifetime of the object.

If we compare invalid programs that compile, which is an important point as well, then those guarantees go out the window. But here Java is much more forgiving than C++: if you have improper synchronization, you may see fields which are null instead of having their final value, which is bad and confusing. But in C++ with improper synchronization, you can see literally any outcome at all.


That's not accurate for a final field, as final fields are initialized in a special way.

https://docs.oracle.com/javase/specs/jls/se21/html/jls-17.ht...

>An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.

If you don't publish a reference to the object from within the constructor, you will not see a null value of the final field, even if the object itself was unsafely published across threads via a non-volatile field.


This too is thanks to JSR-133. In fact, it seems that, thanks to this part of JSR-133, what I said above about marking shared volatile is actually unnecessary. There must be a memory barrier somewhere, under the hood, though.


At least on android arm64, looks like a `dmb ishst` is emitted after the constructor, which allows future loads to not need an explicit barrier. Removing `final` from the field causes that barrier to not be emitted.

https://godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(filename...




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: