Continuing the series, today we’ll dive into local functions, fixed-size buffers, fixed blocks on arbitrary types with GetPinnableReference, and stackalloc initializers to see how they’re all implemented in C++ and what assembly code ends up actually running on the CPU.

Simple Local Functions

Let’s start by looking at a simple example of a local function:

static class TestClass
{
    static int TestLocalFunction(int x, int y)
    {
        int Double(int i)
        {
            return i + i;
        }
 
        return Double(x) + Double(y);
    }
}

TestLocalFunction contains a local function named Double that’s inaccessible from outside TestLocalFunction. Inside, it can call it like any other function. Now let’s see the C++ that IL2CPP generates in Unity 2018.3.0f2:

extern "C" IL2CPP_METHOD_ATTR int32_t TestClass_TestLocalFunction_m205500DBF6C1E6611D60FD1327D02BD678B6071A (int32_t ___x0, int32_t ___y1, const RuntimeMethod* method)
{
    {
        int32_t L_0 = ___x0;
        int32_t L_1 = TestClass_U3CTestLocalFunctionU3Eg__DoubleU7C35_0_m50E0DE5D2952E664E1D30777A97C11F764B392A3(L_0, /*hidden argument*/NULL);
        int32_t L_2 = ___y1;
        int32_t L_3 = TestClass_U3CTestLocalFunctionU3Eg__DoubleU7C35_0_m50E0DE5D2952E664E1D30777A97C11F764B392A3(L_2, /*hidden argument*/NULL);
        return ((int32_t)il2cpp_codegen_add((int32_t)L_1, (int32_t)L_3));
    }
}

Here we see two calls to another function with an extremely long name: TestClass_U3CTestLocalFunctionU3Eg__Double.... Each call passes the appropriate parameter: x or y. Let’s go look at the function being called:

extern "C" IL2CPP_METHOD_ATTR int32_t TestClass_U3CTestLocalFunctionU3Eg__DoubleU7C35_0_m50E0DE5D2952E664E1D30777A97C11F764B392A3 (int32_t ___i0, const RuntimeMethod* method)
{
    {
        int32_t L_0 = ___i0;
        int32_t L_1 = ___i0;
        return ((int32_t)il2cpp_codegen_add((int32_t)L_0, (int32_t)L_1));
    }
}

As expected, this simply adds the parameters together and returns the result. Now let’s look at the assembly code for a 64-bit iOS release build in Xcode 10.1:

add   r0, r1
lsls  r0, r0, #1
bx    lr

Both function calls have been inlined, as have a plethora of redundant copies and the il2cpp_codegen_add call. All we’re left with is a handful of instructions, which is a great result!

Conclusion: Simple nested functions work just like normal functions.

Nested Function Closures

Now let’s layer on a little more complexity and have the nested function access local variables of the function its in without having them passed to it as parameters:

static class TestClass
{
    static int TestLocalFunctionClosure(int x, int y)
    {
        int Sum()
        {
            return x + y;
        }
 
        return Sum() + Sum();
    }
}

Here we see that Sum takes no parameters and instead simply accesses the x and y parameters of the outer TestLocalFunctionClosure function directly. To see how this is implemented behind the scenes, let’s again look at the C++ output from IL2CPP:

extern "C" IL2CPP_METHOD_ATTR int32_t TestClass_TestLocalFunctionClosure_m88629459471AB7F3EB13F64166FDBA9E7AC846B3 (int32_t ___x0, int32_t ___y1, const RuntimeMethod* method)
{
    U3CU3Ec__DisplayClass36_0_tC465B02642D015B4DBF1DC72A470D1BE8B73539C  V_0;
    memset(&V_0, 0, sizeof(V_0));
    {
        int32_t L_0 = ___x0;
        (&V_0)->set_x_0(L_0);
        int32_t L_1 = ___y1;
        (&V_0)->set_y_1(L_1);
        int32_t L_2 = TestClass_U3CTestLocalFunctionClosureU3Eg__SumU7C36_0_m266233C959AFB4048BBBA27B6BB68CBA3257F856((U3CU3Ec__DisplayClass36_0_tC465B02642D015B4DBF1DC72A470D1BE8B73539C *)(&V_0), /*hidden argument*/NULL);
        int32_t L_3 = TestClass_U3CTestLocalFunctionClosureU3Eg__SumU7C36_0_m266233C959AFB4048BBBA27B6BB68CBA3257F856((U3CU3Ec__DisplayClass36_0_tC465B02642D015B4DBF1DC72A470D1BE8B73539C *)(&V_0), /*hidden argument*/NULL);
        return ((int32_t)il2cpp_codegen_add((int32_t)L_2, (int32_t)L_3));
    }
}

Again we see two calls to a long-named function, but this time it’s different. First, we see a structure get allocated and cleared to all zeroes with memset. Then we see its x and y fields set to the x and y parameters of the function. Then a pointer to that structure is passed to the two function calls. This effectively packages up the parameters, just as with other closures such as delegates.

Now let’s see the Sum function:

extern "C" IL2CPP_METHOD_ATTR int32_t TestClass_U3CTestLocalFunctionClosureU3Eg__SumU7C36_0_m266233C959AFB4048BBBA27B6BB68CBA3257F856 (U3CU3Ec__DisplayClass36_0_tC465B02642D015B4DBF1DC72A470D1BE8B73539C * p0, const RuntimeMethod* method)
{
    {
        U3CU3Ec__DisplayClass36_0_tC465B02642D015B4DBF1DC72A470D1BE8B73539C * L_0 = p0;
        int32_t L_1 = L_0->get_x_0();
        U3CU3Ec__DisplayClass36_0_tC465B02642D015B4DBF1DC72A470D1BE8B73539C * L_2 = p0;
        int32_t L_3 = L_2->get_y_1();
        return ((int32_t)il2cpp_codegen_add((int32_t)L_1, (int32_t)L_3));
    }
}

Here we see the opposite: Sum unpacks the structure it was passed to retrieve the x and y values. Then it performs its only action: summing them.

Let’s see how this all comes out in assembly once the C++ compiler has run:

add   r0, r1
lsls  r0, r0, #1
bx    lr

The C++ compiler has correctly determined that all that packaging up into a struct, clearing to zero, and unpacking from the struct could be eliminated and left us with just the same few instructions we had in the first example. This is another great result!

Conclusion: Accessing the outer function’s local variables involves more C++ but results in the same assembly.

Nested Local Functions

Next we’ll nest a local function inside of a local function to see how that works:

static class TestClass
{
    static int TestLocalFunctionClosureNested(int x, int y)
    {
        int Double()
        {
            int Sum()
            {
                return x + y;
            }
 
            return Sum() + Sum();
        }
 
        return Double() + Double();
    }
}

Notice that the innermost function—Sum—accesses the parameters of the outermost function.

Let’s look at the C++ for this:

extern "C" IL2CPP_METHOD_ATTR int32_t TestClass_TestLocalFunctionClosureNested_m6615DF2A6279BEA37DB8D642B8F7D1B6FC9F236A (int32_t ___x0, int32_t ___y1, const RuntimeMethod* method)
{
    U3CU3Ec__DisplayClass37_0_tC7FA4A588F8A8705D87B057EFFE4FCE5B548C718  V_0;
    memset(&V_0, 0, sizeof(V_0));
    {
        int32_t L_0 = ___x0;
        (&V_0)->set_x_0(L_0);
        int32_t L_1 = ___y1;
        (&V_0)->set_y_1(L_1);
        int32_t L_2 = TestClass_U3CTestLocalFunctionClosureNestedU3Eg__DoubleU7C37_0_m2BABBE6FF22738967F469B2981B702691507587B((U3CU3Ec__DisplayClass37_0_tC7FA4A588F8A8705D87B057EFFE4FCE5B548C718 *)(&V_0), /*hidden argument*/NULL);
        int32_t L_3 = TestClass_U3CTestLocalFunctionClosureNestedU3Eg__DoubleU7C37_0_m2BABBE6FF22738967F469B2981B702691507587B((U3CU3Ec__DisplayClass37_0_tC7FA4A588F8A8705D87B057EFFE4FCE5B548C718 *)(&V_0), /*hidden argument*/NULL);
        return ((int32_t)il2cpp_codegen_add((int32_t)L_2, (int32_t)L_3));
    }
}

This looks just like before. We see that the parameters are packaged up into a struct and pointers to it are passed to the first-level nested function: Double. Let’s go look at that:

extern "C" IL2CPP_METHOD_ATTR int32_t TestClass_U3CTestLocalFunctionClosureNestedU3Eg__DoubleU7C37_0_m2BABBE6FF22738967F469B2981B702691507587B (U3CU3Ec__DisplayClass37_0_tC7FA4A588F8A8705D87B057EFFE4FCE5B548C718 * p0, const RuntimeMethod* method)
{
    {
        U3CU3Ec__DisplayClass37_0_tC7FA4A588F8A8705D87B057EFFE4FCE5B548C718 * L_0 = p0;
        int32_t L_1 = TestClass_U3CTestLocalFunctionClosureNestedU3Eg__SumU7C37_1_mEB313FB21349FE3A16EA16DC0E3A85431F0EA176((U3CU3Ec__DisplayClass37_0_tC7FA4A588F8A8705D87B057EFFE4FCE5B548C718 *)L_0, /*hidden argument*/NULL);
        U3CU3Ec__DisplayClass37_0_tC7FA4A588F8A8705D87B057EFFE4FCE5B548C718 * L_2 = p0;
        int32_t L_3 = TestClass_U3CTestLocalFunctionClosureNestedU3Eg__SumU7C37_1_mEB313FB21349FE3A16EA16DC0E3A85431F0EA176((U3CU3Ec__DisplayClass37_0_tC7FA4A588F8A8705D87B057EFFE4FCE5B548C718 *)L_2, /*hidden argument*/NULL);
        return ((int32_t)il2cpp_codegen_add((int32_t)L_1, (int32_t)L_3));
    }
}

Here we see two calls to the innermost function: Sum. We don’t see any more packaging of parameters though, as this simply passes them along via the pointer it’s provided.

Now let’s look at Sum:

extern "C" IL2CPP_METHOD_ATTR int32_t TestClass_U3CTestLocalFunctionClosureNestedU3Eg__SumU7C37_1_mEB313FB21349FE3A16EA16DC0E3A85431F0EA176 (U3CU3Ec__DisplayClass37_0_tC7FA4A588F8A8705D87B057EFFE4FCE5B548C718 * p0, const RuntimeMethod* method)
{
    {
        U3CU3Ec__DisplayClass37_0_tC7FA4A588F8A8705D87B057EFFE4FCE5B548C718 * L_0 = p0;
        int32_t L_1 = L_0->get_x_0();
        U3CU3Ec__DisplayClass37_0_tC7FA4A588F8A8705D87B057EFFE4FCE5B548C718 * L_2 = p0;
        int32_t L_3 = L_2->get_y_1();
        return ((int32_t)il2cpp_codegen_add((int32_t)L_1, (int32_t)L_3));
    }
}

This looks just like in the last example. The pointer to the struct is used to unpack the x and y parameters and then sum them.

Finally, let’s look at the assembly output of the C++ compiler to see how it compares:

add   r0, r1
lsls  r0, r0, #2
bx    lr

Once again, we have the same three instructions. This one’s just slightly different because we end up doubling twice so the left shift (lsls) moves two bits instead of just one.

Conclusion: Nesting local functions works just like a single level of nesting.

Indexing Fixed-Size Buffers

Now for something different: C# 7.3 no longer requires pinning to access a fixed buffer. Here’s what we can write with that:

unsafe struct StructWithFixed
{
    public fixed int Values[10];
}
 
static class TestClass
{
    unsafe static int TestIndexFixed(StructWithFixed swf)
    {
        return swf.Values[5];
    }
}

We still need the unsafe keyword, but we don’t need to use a fixed block to access the fixed-size buffer. Previously, we’d have to write fixed (int* p = swf.Values) return p[5];, but we can omit the first part now.

Let’s see how this looks in C++:

extern "C" IL2CPP_METHOD_ATTR int32_t TestClass_TestIndexFixed_mF48A61F3ECBD054C48E52B3DE7D3D9C6FE38BD2D (StructWithFixed_t1CFD600361B8DE3AF7D15435C01C3A6CE45552F7  ___swf0, const RuntimeMethod* method)
{
    {
        U3CValuesU3Ee__FixedBuffer_tE6059D2CABCEA19680613F8E1EC05977F95090E8 * L_0 = (&___swf0)->get_address_of_Values_0();
        int32_t* L_1 = L_0->get_address_of_FixedElementField_0();
        int32_t L_2 = *((int32_t*)((int32_t*)il2cpp_codegen_add((intptr_t)L_1, (intptr_t)((intptr_t)il2cpp_codegen_multiply((intptr_t)(((intptr_t)5)), (int32_t)4)))));
        return L_2;
    }
}

The first two lines call some accessors to get a pointer to the Values array. That’s helpful because the struct itself is declared in a rather unusual way:

struct  U3CValuesU3Ee__FixedBuffer_tE6059D2CABCEA19680613F8E1EC05977F95090E8 
{
public:
    union
    {
        struct
        {
            // System.Int32 StructWithFixed/<Values>e__FixedBuffer::FixedElementField
            int32_t ___FixedElementField_0;
        };
        uint8_t U3CValuesU3Ee__FixedBuffer_tE6059D2CABCEA19680613F8E1EC05977F95090E8__padding[40];
    };
 
public:
    inline static int32_t get_offset_of_FixedElementField_0() { return static_cast<int32_t>(offsetof(U3CValuesU3Ee__FixedBuffer_tE6059D2CABCEA19680613F8E1EC05977F95090E8, ___FixedElementField_0)); }
    inline int32_t get_FixedElementField_0() const { return ___FixedElementField_0; }
    inline int32_t* get_address_of_FixedElementField_0() { return &___FixedElementField_0; }
    inline void set_FixedElementField_0(int32_t value)
    {
        ___FixedElementField_0 = value;
    }
};

The struct for the fixed buffer contains a union of a struct and an array of 40 bytes. The C# fixed buffer is of 10 int, so this is the right size to hold all of its elements. The struct is of the equivalent element type: int32_t. Since the memory of a union is shared amongst its members, this doesn’t take up any more space than just the 40 bytes of the array.

We also see the get_address_of_FixedElementField_0 accessor that just returns the address of the int32_t, which is a pointer to the first element of the fixed-size buffer.

To finish up the test function, let’s look at a formatted version of the final line:

int32_t L_2 = *((int32_t*)((int32_t*)il2cpp_codegen_add(
    (intptr_t)L_1,
    (intptr_t)((intptr_t)il2cpp_codegen_multiply(
        (intptr_t)(((intptr_t)5)),
        (int32_t)4)))));

There’s a ton of unnecessary casting here, but this ends up as the equivalent of an offset by 20 bytes into the array. That’s right since one int is four bytes and we’re indexing in by five.

With that in mind, let’s look at the assembly:

    push   {r4, r5, r6, r7, lr}
    add    r7, sp, #12
    sub    sp, #44
    movw   r6, :lower16:(L___stack_chk_guard$non_lazy_ptr-(LPC59_0+4))
    mov    r9, r0
    movt   r6, :upper16:(L___stack_chk_guard$non_lazy_ptr-(LPC59_0+4))
    ldr.w  r12, [r7, #16]
LPC59_0:
    add    r6, pc
    ldr.w  lr, [r7, #20]
    ldr    r5, [r7, #28]
    ldr    r6, [r6]
    ldr    r0, [r7, #12]
    ldr    r4, [r7, #24]
    ldr    r6, [r6]
    str    r6, [sp, #40]
    ldr    r6, [r7, #8]
    strd   r9, r1, [sp]
    add    r1, sp, #8
    stm    r1!, {r2, r3, r6}
    add    r1, sp, #20
    stm.w  r1, {r0, r12, lr}
    strd   r4, r5, [sp, #32]
    ldr    r1, [sp, #40]
    movw   r2, :lower16:(L___stack_chk_guard$non_lazy_ptr-(LPC59_1+4))
    movt   r2, :upper16:(L___stack_chk_guard$non_lazy_ptr-(LPC59_1+4))
LPC59_1:
    add    r2, pc
    ldr    r2, [r2]
    ldr    r2, [r2]
    subs   r1, r2, r1
    itt    eq
    addeq  sp, #44
    popeq  {r4, r5, r6, r7, pc}
    bl     ___stack_chk_fail

This is a surprising amount of code! Xcode’s Clang C++ compiler has generated stack protection code for us, despite never generating it for any other C++ snippet so far. That’s disappointing because it’s turned what should be a very cheap read of four bytes into over 30 instructions. This may vary from platform to platform, but it at least affects this 64-bit iOS build.

Conclusion: The generated C++ looks fast, but stack protection code may degrade the performance of accessing fixed buffers.

Stackalloc Initializers

Let’s try out the cousin of fixed buffers: stackalloc. This allows us to create a fixed-size buffer as a local variable rather than a field. In C# 7.3, we can now use initializers just like we would with arrays. Here’s how it looks:

static class TestClass
{
    unsafe static int TestStackallocInitializer()
    {
        int* a = stackalloc[] { 1, 2, 3 };
        return a[0] + a[1] + a[2];
    }
}

The { 1, 2, 3 } part is new, as is the omission of a size in stackalloc[].

Here’s how this looks in C++:

extern "C" IL2CPP_METHOD_ATTR int32_t TestClass_TestStackallocInitializer_m6ED11E7ECB97F2A4C298828F8354629410E08497 (const RuntimeMethod* method)
{
    int32_t* V_0 = NULL;
    {
        int8_t* L_0 = (int8_t*) alloca((((uintptr_t)((int32_t)12))));
        memset(L_0,0,(((uintptr_t)((int32_t)12))));
        int8_t* L_1 = (int8_t*)(L_0);
        *((int32_t*)L_1) = (int32_t)1;
        int8_t* L_2 = (int8_t*)L_1;
        *((int32_t*)((int8_t*)il2cpp_codegen_add((intptr_t)L_2, (int32_t)4))) = (int32_t)2;
        int8_t* L_3 = (int8_t*)L_2;
        *((int32_t*)((int8_t*)il2cpp_codegen_add((intptr_t)L_3, (intptr_t)((intptr_t)il2cpp_codegen_multiply((intptr_t)(((intptr_t)2)), (int32_t)4))))) = (int32_t)3;
        V_0 = (int32_t*)L_3;
        int32_t* L_4 = V_0;
        int32_t L_5 = *((int32_t*)L_4);
        int32_t* L_6 = V_0;
        int32_t L_7 = *((int32_t*)((int32_t*)il2cpp_codegen_add((intptr_t)L_6, (int32_t)4)));
        int32_t* L_8 = V_0;
        int32_t L_9 = *((int32_t*)((int32_t*)il2cpp_codegen_add((intptr_t)L_8, (intptr_t)((intptr_t)il2cpp_codegen_multiply((intptr_t)(((intptr_t)2)), (int32_t)4)))));
        return ((int32_t)il2cpp_codegen_add((int32_t)((int32_t)il2cpp_codegen_add((int32_t)L_5, (int32_t)L_7)), (int32_t)L_9));
    }
}

Here we see a call to alloca, which dynamically allocates space on the stack for local variables. In this case we’re allocating 12 bytes for the three int values.

After that, we see the assignment of 1, 2, and 3 to the first, second, and third elements. Each is assigned differently as the first can simply dereference the pointer to the first element, the second can simply add to get to the second element, and the third requires the full “add and multiply” offset we saw with the fixed buffers example.

At the end we see the reverse: the first, second, and third elements are read from the array and ultimately added together.

Here’s how this all looks in assembly after the C++ compiler runs:

    push   {r7, lr}
    mov    r7, sp
    sub    sp, #16
    movw   r0, :lower16:(L___stack_chk_guard$non_lazy_ptr-(LPC60_0+4))
    movt   r0, :upper16:(L___stack_chk_guard$non_lazy_ptr-(LPC60_0+4))
LPC60_0:
    add    r0, pc
    ldr    r0, [r0]
    ldr    r0, [r0]
    str    r0, [sp, #12]
    movs   r0, #3
    str    r0, [sp, #8]
    movs   r0, #2
    str    r0, [sp, #4]
    movs   r0, #1
    str    r0, [sp]
    ldr    r0, [sp, #12]
    movw   r1, :lower16:(L___stack_chk_guard$non_lazy_ptr-(LPC60_1+4))
    movt   r1, :upper16:(L___stack_chk_guard$non_lazy_ptr-(LPC60_1+4))
LPC60_1:
    add    r1, pc
    ldr    r1, [r1]
    ldr    r1, [r1]
    subs   r0, r1, r0
    ittt   eq
    moveq  r0, #6
    addeq  sp, #16
    popeq  {r7, pc}
    bl     ___stack_chk_fail

Once again we see the “stack protection” code, which is somewhat expected since stackalloc and its related operations looked so similar to those of fixed-size buffers.

Conclusion: Like fixed-size buffers, the C++ code for stackalloc looks fast but can be marred by “stack protection” code.

GetPinnableReference

Finally for today, we’ll try out the new ability in C# 7.3 to use fixed blocks with any type via the new GetPinnableReference method:

class TestClassWithGetPinnableReference
{
    private int X;
 
    public ref int GetPinnableReference()
    {
        return ref X;
    }
}
 
static class TestClass
{
    unsafe static int TestFixedWithGetPinnableReference(
        TestClassWithGetPinnableReference c)
    {
        fixed (int* p = c)
        {
            return *p;
        }
    }
}

Here we have TestClassWithGetPinnableReference that has a GetPinnableReference method returning a ref int. The TestFixedWithGetPinnableReference method is then able to use a fixed block on this class because its GetPinnableReference method will be called to get the pointer to fix.

Here’s the C++ that IL2CPP generates for this:

extern "C" IL2CPP_METHOD_ATTR int32_t TestClass_TestFixedWithGetPinnableReference_m7F7E8B53D6E20E7C1EDD4536E1A845D310841B4C (TestClassWithGetPinnableReference_tC3D655903BC46E3591379A54F2E7B664C3BF6F32 * ___c0, const RuntimeMethod* method)
{
    int32_t* V_0 = NULL;
    uintptr_t G_B3_0;
    memset(&G_B3_0, 0, sizeof(G_B3_0));
    {
        TestClassWithGetPinnableReference_tC3D655903BC46E3591379A54F2E7B664C3BF6F32 * L_0 = ___c0;
        if (L_0)
        {
            goto IL_0007;
        }
    }
    {
        G_B3_0 = (((uintptr_t)0));
        goto IL_0010;
    }
 
IL_0007:
    {
        TestClassWithGetPinnableReference_tC3D655903BC46E3591379A54F2E7B664C3BF6F32 * L_1 = ___c0;
        NullCheck(L_1);
        int32_t* L_2 = TestClassWithGetPinnableReference_GetPinnableReference_m4405E3FDDB912D1BAEC0569AC7E2BF1057952999(L_1, /*hidden argument*/NULL);
        V_0 = (int32_t*)L_2;
        int32_t* L_3 = V_0;
        G_B3_0 = (((uintptr_t)L_3));
    }
 
IL_0010:
    {
        int32_t L_4 = *((int32_t*)G_B3_0);
        return L_4;
    }
}

This function starts off with a null check on the parameter c. If it’s not null then NullCheck is called which will not throw the NullReferenceException. It seems strange to duplicate the null check when it will never do anything, so let’s hope the C++ compiler catches and omits this in the assembly output. If c is null then G_B3_0 is assigned null and then dereferenced in IL_0010 which will crash the game at a minimum or trigger undefined behavior that can result in arbitrary compiler output. That’s a lot more harsh than just a NullReferenceException!

Assuming c isn’t null, we see the call to GetPinnableReference which returns a pointer to an int: int32_t*. That’s dereferenced and the resulting value is returned. Here’s how it all turns out in assembly:

    push   {r4, r7, lr}
    add    r7, sp, #4
    mov    r4, r0
    cbnz   r4, LBB61_2
    movs   r0, #0
    bl     __ZN6il2cpp2vm9Exception27RaiseNullReferenceExceptionEP19Il2CppSequencePoint
LBB61_2:
    ldr    r0, [r4, #8]
    pop    {r4, r7, pc}

In the end, the C++ compiler has generated minimal code plus the null check and throwing a NullReferenceException. It could have been a lot worse with “undefined behavior,” but in this case we’ll just get a managed exception.

Conclusion: GetPinnableReference generates C++ code with “undefined behavior,” but the compiler may still generate good assembly output.

Conclusion

Local functions are a nice addition to the language that allows for “helper” functions to be reduced in scope to just the function they help. The IL2CPP output for this is reasonable and always results in fast, minimal assembly code.

Indexing fixed and stackalloc buffers results in good C++ output, but the C++ compiler may add “stack protection” that greatly adds to the overall cost of the operation. For now, performance critical code would do well to avoid these types of buffers in favor of managed and native arrays.

GetPinnableReference allows for fixed blocks that work on any type. The resulting C++ includes “undefined behavior” in the form of a null dereference, but thankfully the compiler may save this situation and simply throw a managed NullReferenceException.