iangilham.com

By on

software bugs cplusplus

I’ve written before about the compiler bug in managed C++ 7.1. From the information I had available, it looked like the compiler warning was merely a false-positive. Unfortunately, it looks like the problem goes deeper than that.

Consider this test application:

// Managed C++ legacy syntax for .Net 1.0

#include "stdafx.h"

#using <mscorlib.dll>

using namespace System;
using namespace System::Collections;

// commented the FlagsAttribute to simplify printout
//[FlagsAttribute]
__value enum MyEnum : Int64 {
    None = 0,
    Flag0 = 1i64 << 0,
    Flag1 = 1i64 << 1,
    Flag2 = 1i64 << 2,
    Flag3 = 1i64 << 3,
    Flag4 = 1i64 << 4,
    Flag5 = 1i64 << 5,
    Flag6 = 1i64 << 6,
    Flag7 = 1i64 << 7,
    Flag8 = 1i64 << 8,
    Flag9 = 1i64 << 9,
    Flag10 = 1i64 << 10,
    Flag11 = 1i64 << 11,
    Flag12 = 1i64 << 12,
    Flag13 = 1i64 << 13,
    Flag14 = 1i64 << 14,
    Flag15 = 1i64 << 15,
    Flag16 = 1i64 << 16,
    Flag17 = 1i64 << 17,
    Flag18 = 1i64 << 18,
    Flag19 = 1i64 << 19,
    Flag20 = 1i64 << 20,
    Flag21 = 1i64 << 21,
    Flag22 = 1i64 << 22,
    Flag23 = 1i64 << 23,
    Flag24 = 1i64 << 24,
    Flag25 = 1i64 << 25,
    Flag26 = 1i64 << 26,
    Flag27 = 1i64 << 27,
    Flag28 = 1i64 << 28,
    Flag29 = 1i64 << 29,
    Flag30 = 1i64 << 30,
    Flag31 = 1i64 << 31,
    Flag32 = 1i64 << 32,
    Flag33 = 1i64 << 33,
    Flag34 = 1i64 << 34,
    Flag35 = 1i64 << 35,
    Flag36 = 1i64 << 36,
    Flag37 = 1i64 << 37,
    Flag38 = 1i64 << 38,
    Flag39 = 1i64 << 39,
    Flag40 = 1i64 << 40,
    Flag41 = 1i64 << 41,
    Flag42 = 1i64 << 42,
    Flag43 = 1i64 << 43,
    Flag44 = 1i64 << 44,
    Flag45 = 1i64 << 45,
    Flag46 = 1i64 << 46,
    Flag47 = 1i64 << 47,
    Flag48 = 1i64 << 48,
    Flag49 = 1i64 << 49,
    Flag50 = 1i64 << 50,
    Flag51 = 1i64 << 51,
    Flag52 = 1i64 << 52,
    Flag53 = 1i64 << 53,
    Flag54 = 1i64 << 54,
    Flag55 = 1i64 << 55,
    Flag56 = 1i64 << 56,
    Flag57 = 1i64 << 57,
    Flag58 = 1i64 << 58,
    Flag59 = 1i64 << 59,
    Flag60 = 1i64 << 60,
    Flag61 = 1i64 << 61,
    Flag62 = 1i64 << 62,
    Flag63 = 1i64 << 63,
    All = Int64::MaxValue
};

int _tmain()
{
    Console::WriteLine(S"Testing enum range");
    Array* values = Enum::GetValues(__typeof(MyEnum));
    for(Int32 i = 0; i < values->Length; ++i) {
        Object* o = values->GetValue(i);
        Console::WriteLine("{0} = {1}",
            Enum::Format(__typeof(MyEnum), o, "G"),
            Enum::Format(__typeof(MyEnum), o, "D"));
    }
    return 0;
}

If you build that as a Managed C++ project in Visual Studio .Net 2003, you will see the telltale signs of compiler integer truncation:

Testing enum range
Flag33 = 0
Flag33 = 0
...
Flag0 = 1
Flag1 = 2
Flag2 = 4
...
Flag31 = 2147483648
All = 4294967295

This is what the dreaded compiler warning was telling us about in the first place, and sure enough, we got the warning while compiling this code: warning C4309: 'initializing' : truncation of constant value. We seem to have come full circle, verifying that despite being told to use Int64 to back the enum, the compiler decided to use an Int32 anyway and made a mess of things.

It gets stranger than that. I revisited this problem because I received a bug report where the enum has been opened up, and found to contain silly values like 0x102000000L as well as duplicates. The strange ones are None and All:

None = 0x100000000L,
All = 0x1ffffffffL,

A 64-bit integer is clearly being used for the storage backing of the enum, but the values are wrong and only the first 32 bits seem to be read when reading values in code. The only sane conclusion is that using a 64 bit integer to back a managed enum in Managed C++ invoked undefined behaviour in the Visual C++ 7.1 compiler.

This issue is fixed in VC8 using /clr:oldSyntax to compile the same code. I’d love to be able to tell all my clients to upgrade, but in order to keep everyone happy without requiring too much client code to change, I’ll have to settle for using static constants inside a little class:

public class EnumConstantFlags {
public:
    static const Int64 None = 0;
    static const Int64 Flag0 = 1i64 << 0;
    static const Int64 Flag1 = 1i64 << 1;
    // ...
};

These constants can still be used as a bitfield, so the damage is minimal and it will work on all versions of the compiler and CLR.