5

First the history:

I found out DateTime data types cannot be null (Yes they can be nullable using the ? nullable declaration).

I was told this is because they are value types and placed on the stack (not on the heap).

I can understand why the stack is quicker and why value types need to (mostly) be placed on the stack, what I do not know is.

  1. Is it true the real limitation of why a value type cannot be null is because the stack cannot handle null values?

  2. If yes - Why? If No what is the real limitation.

I understand in .net we often don't care about exact memory allocation, yet it is important in performance applications to take the stack and heap into consideration, so I'm trying to wrap my head around this.

Thanks in advance for your expertise on a difficult question.

EDIT: Another thing to add are nullable types hence placed on the heap or stack?

4
  • 7
    Forget about stack and heap. Seriously. In programming languages with fully automatic memory management ("managed"), the distinctions is an implementation detail. And if you're bent on thinking in these terms, at least seperate them from value/reference types. For instance: A value types doesn't need to be placed on the stack (in fact, they frequently aren't - see the linked article).
    – user7043
    Commented Nov 3, 2011 at 19:08
  • 1
    This article may interest you. EDIT: Ninja'd :D Commented Nov 3, 2011 at 19:15
  • 1
    The title of the question is quite misleading - the title asks why null types can't be placed on the stack, but the body of the question asks why value types can't be null.
    – Arjailer
    Commented Nov 3, 2011 at 19:36
  • You are essentially asking why cant int's (and floats) be null. Sure they can be 0 but 0 is not null.
    – user2528
    Commented Nov 4, 2011 at 5:43

2 Answers 2

22

The heap and stack are not relevant. It is about Binary representation of values.

In a reference type, a zero binary representation means null, and other binary representations point to memory that contains the binary representation of the value.

In a value type, all binary representations, including zero, directly represent a particular value of the type. Value types cannot be null because there is no reserved binary representation that means null.

16
  • 2
    I hate to break it to you but int? or if you prefer Nullable<int> are indeed value types. And yet they support the value = null semantics. The binary representation of a value type has nothing to do with its semantics. Commented Nov 3, 2011 at 21:00
  • 4
    @Conrad, yes the Nullable<T> type itself defines representations for null. This a feature of that particular type and not of value types, like null values are a feature of reference types. I meant to answer the question of why in general value types cannot be null, not claim that you couldn't make a value type that could represent null.
    – JGWeissman
    Commented Nov 3, 2011 at 21:07
  • 1
    @Conrad: You're wrong. int? has a special extra boolean field that indicates if it is null or not. int itself cannot be null, as there is no binary representation that indicates null.
    – DeadMG
    Commented Nov 3, 2011 at 21:07
  • @DeadMG int? x= null; works fine for me. Is that somehow different to you than object x = null; Commented Nov 3, 2011 at 21:10
  • 2
    @Conrad, .Net languages such as C# provide syntactic sugar for the Nullable<T> type to allow treating a Nullable<T> value with HasValue = false the same as a null reference type. However, having worked with MSIL code directly, I can tell you there are differences. Like if I want to load a null value for a reference type onto the stack, I can use "ldnull", but this does not work for Nullable<T>.
    – JGWeissman
    Commented Nov 3, 2011 at 21:18
9

I found out DateTime data types cannot be null.

Your questions show that you have a pretty vague idea about how the whole system works. Let's start by picking apart what you mean by "DateTime data types cannot be null". I think what you mean here is that the null literal cannot be implicitly converted to a value of DateTime, and hence the null literal cannot be assigned to a variable of type DateTime.

That is:

DateTime d = null;

is not legal in C#. The null literal cannot be converted to DateTime, and so the initialization must fail at compile time.

I was told this is because they are value types...

That is almost correct.

There are two kinds of value types: nullable value types and non-nullable value types. A nullable value type always has an "underlying" type. The underlying type is always a non-nullable value type.

It is always legal to implicitly convert a null literal to a nullable value type.

It is almost always illegal to implicitly convert a null literal to a non-nullable value type; the only time this happens is if someone creates a user-defined conversion operator on the non-nullable value type, and that user-defined conversion operator takes an argument to which the null literal is implicitly convertible.

... and placed on the stack (not on the heap).

This is completely and utterly wrong. Whomever told you that does not know what they are talking about. This makes no sense whatsoever. Whether a null literal is convertible to a type or not at compile time has nothing whatsoever to do with where in memory the data is going to be stored at runtime.

I can understand why the stack is quicker and why value types need to (mostly) be placed on the stack

Then you are misunderstanding something. Values of value types need not be "mostly" placed on the stack.

Values of value type, and references to values of reference type, can sometimes be put on the short term allocation pool when it is known at compile time that the lifetimes of the storage for those values is going to be short. At runtime, that short term pool might be realized as stack locations or registers.

Whether data goes on the short term pool or the long term pool has nothing whatsoever to do with whether the data is a value of value type or a reference. They are not called the "value pool" and the "reference pool". They are the short term pool and the long term pool. Obviously which pool is used has only to do with the answer to the question "do we know ahead of time when we are going to be done using this value or reference?" If the answer is "yes", then the value or reference can go on the short term pool. If the answer is "no", then it has to go on the long term pool.

Is it true the real limitation of why a value type cannot be null is because the stack cannot handle null values?

No, absolutely not. That makes no sense.

What is the real limitation?

Let's look at an easier example than DateTime. Let's look at int. An int is four bytes, no more, no less. Suppose you have a variable of type int. It takes up four bytes. It might take up four bytes on the heap, four bytes on the stack, four bytes in a register -- doesn't matter. It is four bytes of storage, somewhere.

There are 232 possible values you can put into an integer: 0, 1, -1, 2, -2, ... and so on. When you say:

int x = null;

what value do you suppose is going to go into those four bytes of storage? There are only 232 possible values of four bytes, and they are already taken up by the numbers! You can't put six pigeons into five pigeonholes and still have only one pigeon in every pigeonhole. You can't put 232 + 1 values into 32 bits; you need at least 33 bits, and that's one more bit than we've got available.

Non-nullable value types cannot be null because there are exactly as many possible values of a value type as their bits are capable of holding. A distinct "null" value requires one more value than the maximum number the bits are capable of holding.

So how then do we do it with references? Simple; one of the references is reserved as "special"; the reference whose bits are all zero is set aside as never a valid reference. With ints we have a requirement that all 232 values be valid; with references implemented, say, as 32 bit pointers, we have no requirement whatsoever that all 232 possible pointers be valid; the vast majority of them are invalid.

Why don't we do that with value types? Because then:

int x = null;

is exactly the same as

int x = 0;

and it seems bogus to say that "null" just means "the zero of this type". Null should be a distinct value from every other value. (If you want to get the "zero" of a value type you can always say default(T) where T is the type. So int x = default(int); is the same as int x = 0;)

So how then do we do it with nullable value types? We add more bits. When you say:

int? x = null;
int? y = 123;

that is exactly the same as saying:

int x = 0;
bool xHasValue = false;
int y = 123;
bool yHasValue = true;

When you use a nullable value type, you are doing nothing more than passing around a non-nullable value type along with a flag that says whether it is null or not.

I understand in .net we often don't care about exact memory allocation, yet it is important in performance applications to take the stack and heap into consideration, so I'm trying to wrap my head around this.

That is important in some scenarios, yes. But it has nothing whatsoever to do with the question of why the null literal cannot be assigned to a non-nullable value type.

are nullable types hence placed on the heap or stack?

Values of nullable value types are placed wherever the compiler thinks best; they are not different from any other value of any other type.

If the compiler knows that a value will never be used again after a short period then the compiler generates code to place the value on the short-term storage. At runtime, this will be realized as a location on the stack or a register. If the compiler does not know that for sure then it generates code to place the value on the long-term storage heap.

Again, where the storage for a value is located has nothing whatsoever to do with the type of that value. It has only to do with the answer to the question "do I know for sure how long this value needs to hang around?" If the answer is "yes, and it is a short time", then the storage goes on the short-term pool. If the answer is "yes, and it is a long time" or "no, I don't know", then the storage goes on the long-term pool.

11
  • 2
    @ConradFrix: Implementations of other type systems manage to put six pigeons into five pigeonholes and end up with one pigeon per hole? I would love to see such a type system! I could do all kinds of great things with something that could violate basic laws of arithmetic with impunity! But sadly, there is no such beast. Implementations of type systems such as you describe either have hidden bits somewhere to track nullability -- as Nullable<T> does in .NET -- or they change the range of a value such that not all bit patterns refer to a legal value -- as IEEE floats do to define NaN values. Commented Nov 4, 2011 at 20:05
  • 3
    @ConradFrix: Here are your choices: (1) Make int a 32 bit value; reserve some of those values for null, making .NET int incompatible with many existing languages and thereby preventing platform adoptions, (2) Make a "32 bit" integer take up more than 32 bits; take an enormous performance hit every time you use an integer on a 32 bit machine; hardly anyone does that, right? or (3) make int non-nullable. Which would you choose? Commented Nov 4, 2011 at 20:28
  • 5
    @ConradFrix: Now, if we're talking about decisions made in the past that we regret, I regret this one. It is deeply unfortunate that we chose to make nullable reference types and non-nullable value types. History has shown that ideally the type system would make how the value is copied and whether the type supports a missing value orthogonal. It would have made my life easier had .NET had nullable value types and non nullable reference types from day one. But you can't do everything in v1, and remember, we didn't even have generics. Commented Nov 4, 2011 at 20:31
  • 1
    @ConradFrix: If the integer is nullable then there is an extra bit stored somewhere, I promise. Commented Dec 1, 2011 at 20:19
  • 1
    @ConradFrix: I Googled around but couldn't find any information about how a primitive scalar is actually stored in memory. The closest I can get is SqlInt32 (msdn.microsoft.com/en-us/library/…). Reflector tells me it has has 2 fields: an int and a bool.
    – ligos
    Commented Dec 2, 2011 at 2:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.