Debunking another myth about value types

Debunking another myth about value types

Rate This
  • Comments 40

Here's another myth about value types that I sometimes hear:

"Obviously, using the new operator on a reference type allocates memory on the heap. But a value type is called a value type because it stores its own value, not a reference to its value. Therefore, using the new operator on a value type allocates no additional memory. Rather, the memory already allocated for the value is used."

That seems plausible, right? Suppose you have an assignment to, say, a field s of type S:

s = new S(123, 456);

If S is a reference type then this allocates new memory out of the long-term garbage collected pool, a.k.a. "the heap", and makes s refer to that storage. But if S is a value type then there is no need to allocate new storage because we already have the storage. The variable s already exists and we're going to call the constructor on it, right?

Wrong. That is not what the C# spec says and not what we do. (Commenter Wesner Moise points out that yes, that is sometimes what we do. More on that in a minute.)

It is instructive to ask "what if the myth were true?" Suppose it were the case that the statement above meant "determine the memory location to which the constructed type is being assigned, and pass a reference to that memory location as the 'this' reference in the constructor". Consider the following class defined in a single-threaded program (for the remainder of this article I am considering only single-threaded scenarios; the guarantees in multi-threaded scenarios are much weaker.)

using System;
struct S
{
    private int x;
    private int y;
    public int X { get { return x; } }
    public int Y { get { return y; } }
    public S(int x, int y, Action callback)
    {
        if (x > y)
            throw new Exception();
        callback();
        this.x = x;
        callback();
        this.y = y;
        callback();
    }
}

We have an immutable struct which throws an exception if x > y. Therefore it should be impossible to ever get an instance of S where x > y, right? That's the point of this invariant. But watch:

static class P
{
    static void Main()
    {
        S s = default(S);
        Action callback = ()=>{Console.WriteLine("{0}, {1}", s.X, s.Y);};
        s = new S(1, 2, callback);
        s = new S(3, 4, callback);
    }
}

Again, remember that we are supposing the myth I stated above to be the truth. What happens?

* First we make a storage location for s. (Because s is an outer variable used in a lambda, this storage is on the heap. But the location of the storage for s is irrelevant to today's myth, so let's not consider it further.)
* We assign a default S to s; this does not call any constructor. Rather it simply assigns zero to both x and y.
* We make the action.
* We (mythically) obtain a reference to s and use it for the 'this' to the constructor call. The constructor calls the callback three times.
* The first time, s is still (0, 0).
* The second time, x has been mutated, so s is (1, 0), violating our precondition that X is not observed to be greater than Y.
* The third time s is (1, 2).
* Now we do it again, and again, the callback observes (1, 2), (3, 2) and (3, 4), violating the condition that X must not be observed to be greater than Y.

This is horrid. We have a perfectly sensible precondition that looks like it should never be violated because we have an immutable value type that checks its state in the constructor. And yet, in our mythical world, it is violated.

Here's another way to demonstrate that this is mythical. Add another constructor to S:

    public S(int x, int y, bool panic)
    {
        if (x > y)
            throw new Exception();
        this.x = x;
        if (panic)
            throw new Exception();
        this.y = y;
    }
}

We have

static class P
{
    static void Main()
    {
        S s = default(S);
        try
        {
            s = new S(1, 2, false);
            s = new S(3, 4, true);
        }
        catch(Exception ex)
        {
            Console.WriteLine("{0}, {1}", s.X, s.Y);};
        }
    }
}

Again, remember that we are supposing the myth I stated above to be the truth. What happens? If the storage of s is mutated by the first constructor and then partially mutated by the second constructor, then again, the catch block observes the object in an inconsistent state. Assuming the myth to be true. Which it is not. The mythical part is right here:

Therefore, using the new operator on a value type allocates no additional memory. Rather, the memory already allocated for the value is used.

That's not true, and as we've just seen, if it were true then it would be possible to write some really bad code. The fact is that both statements are false. The C# specification is clear on this point:

"If T is a struct type, an instance of T is created by allocating a temporary local variable"

That is, the statement

s = new S(123, 456);

actually means:

* Determine the location referred to by s.
* Allocate a temporary variable t of type S, initialized to its default value.
* Run the constructor, passing a reference to t for "this".
* Make a by-value copy of t to s.

This is as it should be. The operations happen in a predictable order: first the "new" runs, and then the "assignment" runs. In the mythical explanation, there is no assignment; it vanishes. And now the variable s is never observed to be in an inconsistent state. The only code that can observe x being greater than y is code in the constructor. Construction followed by assignment becomes "atomic"(*).

In the real world if you run the first version of the code above you see that s does not mutate until the constructor is done. You get (0,0) three times and then (1,2) three times. Similarly, in the second version s is observed to still be (1,2); only the temporary was mutated when the exception happened.

Now, what about Wesner's point? Yes, in fact if it is a stack-allocated local variable (and not a field in a closure) that is declared at the same level of "try" nesting as the constructor call then we do not go through this rigamarole of making a new temporary, initializing the temporary, and copying it to the local. In that specific (and common) case we can optimize away the creation of the temporary and the copy because it is impossible for a C# program to observe the difference! But conceptually you should think of the creation as a creation-then-copy rather than a creation-in-place; that it sometimes can be in-place is an implementation detail that you should not rely upon.

----------------------------

(*) Again, I am referring to single-threaded scenarios here. If the variable s can be observed on different threads then it can be observed to be in an inconsistent state because copying any struct larger than an int is not guaranteed to be a threadsafe atomic operation.

 

  • But what are you supposed to do if your desired invariant is one that "0, 0" cannot meet?

    You are supposed to either not use a value type or abandon that invariant, or put more stuff in the struct that enables you to deal with the situation.

    For example, suppose you have a value type that represents a handle. You might put some logic in the non-default constructor that verifies with the operating system that the arguments passed in to the constructor allow the internal state to be set to a valid handle. Code which receives a copy of the struct from an untrusted caller cannot assume that a non-default constructor has been called; a non-trusted caller is always allowed to create an instance of a struct without calling any methods on the struct that ensure that its internal state is valid. Therefore when writing code that uses a struct you are required to ensure that the code properly handles the case where the struct is in its default "all zero" state. 

    It is perfectly acceptable to have a flag in the struct which indicates whether the non-default constructor was called and the invariants were checked. For example, the way that Nullable<T> handles this is it has a flag that indicates whether the nullable value was initialized with a valid value or not. The default state of the flag is "false". If you say "new Nullable<int>()" you get the nullable int with the "has value" flag set to false, rather than the value you get with "new Nullable<int>(0)", which sets the flag to true.

    - Eric

  • Somewhere in the back of my mind, I had this inkling that new struct could be used (inside an unsafe block) to create a pointer to a struct instance (like malloc), like the myth discussed above. I wonder what's the C# equivalent of C++'s new, that would return a pointer to newly allocated "heap" memory for a struct instance.

    (Should my descent into insanity ever prompt me to want to do such a thing.)

    Sure, that's easy. To get a pointer to a heap allocated instance of struct S, simply create a one-element array "new S[] { new S(whatever) }", and then use the "fixed" statement to obtain a pointer to the first element of the array. And there you go; you've got a pointer to a pinned, heap-allocated struct. - Eric

  • Is there a reason why you cannot do this instead?

    * Determine the location referred to by s.
    * Set the memory behind s to all zeros (i.e., clear all fields)
    * Run the constructor, passing a reference to s for "this".

    Igor

    How does that technique solve the problem? You still end up with situations in which violations of a given invariant might be observable from outside the constructor. And don't forget about exceptions. Suppose a constructor throws an exception halfway through construction, and you catch the exception. The memory could be half initialized and half still zero if we did it your way. - Eric

  • Nevermind, ignore my comment above.

    I am still surprised that the C# compiler does this, though. I would have thought that calling into unknown code from within a constructor is the anti-pattern that breaks the example code.

    It is a bad pattern and you should probably not do it. The code shown is deliberately contrived so as to more clearly demonstrate the issue. The rules of the language are designed so that you don't run into this sort of problem in realistic, subtle, non-contrived situations, not so that you can do obviously dangerous stuff like invoke arbitrary functions from inside ctors. - Eric

  • Eric, without your callback, JIT could have optimized this assignment out.

    Perhaps, as a benefit of a doubt, that's what people meant.

    While from a general correctness the statement is incorrect.

  • @Dmitiry

    My view is that people rarely quote things like this as a simplification, but instead as a misunderstanding.

  • So, would you mind explaining what (in your mind) structs ARE for? I think the reason why these myths persist is because people want rules of thumb for when structs should be used instead of classes, and one possible rule of thumb is, "When you want to avoid dynamic allocation of memory." But if that's not valid, then when IS a good time to use a struct instead of a class?

    "Dynamic allocation of memory" is a strange thing to want to avoid. Almost everything not done at compile time allocates memory! You add x + y, and the result has to go somewhere; if it cannot be kept in registers then memory has to be allocated for the result. (And of course registers are logically simply a limited pool of preallocated memory from which you can reserve a small amount of space temporarily.)

    Sometimes the memory is allocated on the stack, sometimes it is allocated on the heap, but believe me, it is allocated. Whether the result is a value type or a reference type is irrelevant; values require memory, references require memory, and the thing being referenced requires memory. Values take up memory, and computation produces values.

    The trick is to work out what exactly it is that you're trying to avoid when doing a performance optimization. Why would you want to avoid dynamic allocation? Allocation is very cheap! You know what's not cheap? Garbage collection of a long-lived working set with many small allocations forming a complex reference topology. Don't put the cart before the horse; if what you want to avoid is expensive collections then don't say that you're using structs to avoid dynamic allocation, because you're not. There are lots of strategies for making garbage collections cheaper; some, but not all of them, involve aggressively using structs.

    To address your actual question: you use value types when you need to build a model of something that is logically an immutable value, and, as an implementation detail, that consumes a small fixed amount of storage. An integer between one and a hundred is a value. True and false are values. Physical quantlties, like the approximate electric field vector at a point in space, are values. The price of a lump of cheese is a value. A date is a value. An "employee" type is not a good candidate for a value type; the storage for an employee is likely to be large, mutable, and the semantics are usually referential; two different variables can refer to the same employee.

    - Eric

  • M.E.,

    Use Value Types (structs) when you want Value Type semantics i.e. 'Copy Value' during assignments. I have never felt the need to avoid 'dynamic allocation of memory' and reached 'structs' as the tool to address the requirement.

  • I was not encouraging the "avoiding dynamic allocation" mindset, I was simply saying that some people (people who grew up on C) think that way. It's analogous to the belief that high-performance code must be written in assembly (which I'm aware is also not true).

    To me, the value type vs. reference type decision when defining a new type of object really encompasses four decisions:

    1. Do I want the type to be immutable?

    2. Do I want the type to be nullable?

    3. Do I want to make derived objects?

    4. Do I want to pass the object by value or by reference?

    It is my contention that if I could control 1-3 individually, I would hardly ever care about 4, and I would mix and match 1-3 depending on my needs.

    I can already control #3 for objects with the sealed keyword, so I can always make a reference type either sealed or unsealed, but I can't make a value type unsealed (sometimes inconvenient, but I can't say I yearn for it).

    For nullability, I sometimes want my value type variables to be nullable (which I can do with the nullable operator), but sometimes I want variables containing reference types to be non-nullable (which I can't do, but sometimes makes sense, e.g. for a name field, or for a collection).

    Value types are always "immutable" in a certain sense, but reference types can never be made immutable. If only I had a readonly keyword that means "all instance variables of this object must be readonly", that would take care of #1 (I've often wished for this, I'm sure you have a ready explanation why this was considered and rejected).

    Do you see what I mean? It seems like a relatively uninteresting implementation detail (pass by value vs. pass by reference) is being overloaded to drive much more interesting design decisions.

    If I could write "public sealed readonly class CostOfALumpOfCheese" and then declare a non-nullable variable "CostOfALumpOfCheese! cheeseCost" (where '!' is the opposite of '?'), I would never say "Hey, I should really take this public sealed readonly class and change it to a public struct."

  • M.E. now with .Net 4, you also have to consider variance. A struct is always invariant, meaning that you can't return an IEnumerable<Foo> from a function that is supposed to return an IEnumerable<object> if Foo is a struct but you can if Foo is a class.

    Another consideration is size. Even if a value is immutable, nonnullable, and sealed, if it's 100 bytes in size then you still probably want it to be a class because it would be pretty inefficient as a struct.

  • I am surprised that more people haven't mentioned a key difference between reference types and value types is that value types (can be) blittable. Baring strings/stringbuilders (which are designed quite carefully to allow it if desired) this is only possible on structs.

    Another facet of them, with similar technical reasons to the blittability, is that they have no overhead in size terms bar their payload (and optionally packing) and being multiples of 8bits.

  • Hi, Eric.

    You wrote: "you use value types when you need to build a model of something that is logically an immutable value, and, as an implementation detail, that consumes a small fixed amount of storage"

    But, for example types System.Drawing.Rectangle and System.Windows.Rect are both structures, but at the same time they have methods which mutates theirs content (for example method Inflate).

    May you know why these two types from Winforms and WPF were made mutable structures?

    And may be you can point some cases when actually it is ok to make mutable value types?

  • While I do agree about creating a temp and copying it to its final destination is the way to go, I do not agree with the demonstration, and especially not with the example.

    Clearly, your example code is severely flawed. If your invariant is that x>y, then you cannot call the callback between the assignment of this.x and this.y. If you do so, you are calling an external method while having some object whose invariant is not maintained. A truly invariant-checking compiler would insert a check of the invariant both before and after each call of the callback, and your program would throw an exception.

    What your code does is provide a pre-condition on the arguments of the constructor, not a real invariant.

  • I think this shows that noone, anywhere has ever understood value types :). As a C++ veteran (in the PTDS sense :) ), I would have expected the "unexpected" behaviour. I get that you want to reduce unexpected behaviour, but this seems to be trying a little too hard.

    BTW, this is easily demonstrated by:

    using System;

    unsafe struct Foo

    {

       public Foo(int dummy)

       {

           fixed (Foo* pinThis = &this) Console.WriteLine(new IntPtr(pinThis).ToString("X"));

       }

       static void Main()

       {

           var foo = new Foo(1);

           Console.WriteLine(new IntPtr(&foo).ToString("X"));

       }

    }

    This rule suggests "this" is in practice fixed in a value-type constructor, is it not simply for consistancy with other function-like members?

  • "as an implementation detail, that consumes a small fixed amount of storage"

    I find this the most pressing argument for using structs, since to my mind this 'detail' makes structs play nice with interop and (graphics) hardware. I realize this is probably a niche case, but when choosing between classes and struct I find I first consider if I need a fixed memory layout. My typical 2nd criterium is whether using structs would make life easier on the GC, which can be an important issue on the compact framework, but your treatise has me seriously doubting what I think I know about deterministic finalization.

Page 1 of 3 (40 items) 123