Managed C++ Code Generation Bug?

G

Gerhard Menzl

I have spent the better part of three working days isolating a most
puzzling defect in my Windows Forms application. The minimum code
required to reproduce the problem is:

// StdMaxBug.h

#pragma once

#include <algorithm>

namespace StdMaxBug
{
public __gc struct S
{
static int const s = 8;
};

void works ()
{
int i = 6;
int j = S::s;
int m = std::max (i, j);
}

void doesnt ()
{
int i = 6;
int m = std::max (i, S::s);
}

void test ()
{
works ();
doesnt ();
}
};

Calling test() from anywhere within your minimum Windows Forms
application causes a System.MissingFieldException to be thrown. The
error message is

Field not found: ?.s.

Note that the code works as expected as long as the value of the
constant S::s is copied into a variable first. It is only when the
constant is passed directly to std::max (or std::min, for that matter)
that the exception occurs.

What is extremely irritating (and was mainly repsonsible for taking
three days) is that the debugger cannot be used to identify the problem
because the exception is thrown as soon as you try to step into
doesnt(). Setting a breakpoint at the beginning of the function doesn't
help either.

Is this yet another subtle Managed C++ quirk I have overlooked? Or does
the code generator/JIT compiler/whatever simply go haywire?

An authoritative answer would be most welcome.

Gerhard Menzl
 
T

tom_usenet

I have spent the better part of three working days isolating a most
puzzling defect in my Windows Forms application. The minimum code
required to reproduce the problem is:

I know very little about managed C++, so this answer is purely in
terms of C++...
// StdMaxBug.h

#pragma once

#include <algorithm>

namespace StdMaxBug
{
public __gc struct S
{
static int const s = 8;
};

Note you haven't defined s, as in:
int const S::s;
Not defining a static variable that you use in a program is undefined
behaviour, no diagnostic required.
void works ()
{
int i = 6;
int j = S::s;

The above assigns the value of S::s to j, which counts as use of S::s.
int m = std::max (i, j);
}

void doesnt ()
{
int i = 6;
int m = std::max (i, S::s);

The above passes S::s by const reference. This also counts as use (and
is more problematic, since passing an object by reference conceptually
involves taking its address).
}

void test ()
{
works ();
doesnt ();
}
};

Calling test() from anywhere within your minimum Windows Forms
application causes a System.MissingFieldException to be thrown. The
error message is

Field not found: ?.s.

Right, you haven't defined s.

The reason that "works" "works" and "doesnt" doesn't is that the
compiler is optimizing the use of the value of the constant in "works"
by directly placing the value of the constant in the code. In
"doesn't", S::s is passed by const reference to max, and you can't
pass a non-existent object by reference.

There is one final point - if you use S::s only in contexts that
require integral constant expressions (such as array bounds, template
parameters, assigning to other static const integral members), then
you don't need a definition (according to the 2003 update to the C++
standard). e.g. this is legal

struct S
{
static int const s = 8;
};

int array[S::s];

Tom
 
G

Gerhard Menzl

tom_usenet said:
I know very little about managed C++, so this answer is purely in
terms of C++...

If this were Standard C++, I would never have run into the problem, but
thanks anyway.
Note you haven't defined s, as in:
int const S::s;
Not defining a static variable that you use in a program is undefined
behaviour, no diagnostic required.

This is what every sane C++ developer would think of, and this was the
way I had initially coded it, but alas, this is the .NET zone where
strange creatures lurk and the ordinary laws of nature do not apply. S
is a managed struct, and defining s at namespace scope yields

error C3366: 'StdMaxBug::S::s' : static data members of managed
types must be defined within the class definition

Besides, the constants (there's a whole bunch of them in my real struct)
work perfectly well. It's only when you pass one of them to std::max or
std::min that hell breaks loose.

Still, your hint made me wonder whether the problem might be related to
S being a __gc struct (i.e. only instantiable on the managed heap), even
though no instance is ever created. So I tried

public __value struct S ...

but the result was the same.
Right, you haven't defined s.

Er, according to the logic of the abomination named Managed C++ I have.
The reason that "works" "works" and "doesnt" doesn't is that the
compiler is optimizing the use of the value of the constant in "works"
by directly placing the value of the constant in the code. In
"doesn't", S::s is passed by const reference to max, and you can't
pass a non-existent object by reference.

But I can pass a temporary object. Anyway, even if this were the source
of the problem (which it isn't, see above), I would expect the
*compiler* to raise an exception (read: diagnostic), not the runtime.

Any Managed C++ gurus listening to this?

Gerhard Menzl
 
T

tom_usenet

If this were Standard C++, I would never have run into the problem, but
thanks anyway.

Ahh, I see.
This is what every sane C++ developer would think of, and this was the
way I had initially coded it, but alas, this is the .NET zone where
strange creatures lurk and the ordinary laws of nature do not apply. S
is a managed struct, and defining s at namespace scope yields

error C3366: 'StdMaxBug::S::s' : static data members of managed
types must be defined within the class definition

Besides, the constants (there's a whole bunch of them in my real struct)
work perfectly well. It's only when you pass one of them to std::max or
std::min that hell breaks loose.

Ok, I've just managed to compile it as managed C++.
Still, your hint made me wonder whether the problem might be related to
S being a __gc struct (i.e. only instantiable on the managed heap), even
though no instance is ever created. So I tried

public __value struct S ...

but the result was the same.

I think this must be to do with the CLRs handling of C++ static const
members.
Er, according to the logic of the abomination named Managed C++ I have.

I think the problem is related to the fact that it is a const int, and
doesn't therefore exist (it doesn't have an address). Much simpler
code generates the bug:

int const& cref = S::s;
But I can pass a temporary object.

Yes, assigning the value to a temporary object doesn't require taking
the address of S::s.

Anyway, even if this were the source
of the problem (which it isn't, see above), I would expect the
*compiler* to raise an exception (read: diagnostic), not the runtime.

I think the compiler can't distinguish between a ordinary const
variable and a integral constant expression with no address. In
support of my theory, making S::s non-const removes the error too.

#pragma once
#using <mscorlib.dll>

public __gc struct S
{
static int const s = 8;
static double const d = 5.0;
};

void test()
{
int const& ref = S::s;
//&S::s;
//double const& dref = S::d;
}

int main()
{
test();
}

Interestingly the dref line and the &S::s line won't compile, which
seems to imply that its a compiler bug - you shouldn't be able to bind
the static const int member of a __gc class to a C++ reference, but it
is letting you anyway.

[..time passes..]

I've just researched a bit about the CLR, and it appears that S::s
above is a "static literal field contract attribute", and as such,
doesn't occupy any memory and doesn't exist at runtime, only at
compile time. This is why you can't bind it to a reference, but can
use its value. Basically, you can't use it as an lvalue, only as an
rvalue. I would have thought that the compiler should flag binding to
a reference as an error though.

Tom
 
G

Gerhard Menzl

tom_usenet said:
I think this must be to do with the CLRs handling of C++ static const
members.
[...]


I think the problem is related to the fact that it is a const int, and
doesn't therefore exist (it doesn't have an address). Much simpler
code generates the bug:

int const& cref = S::s;

This would be perfectly ok in Standard C++.
I think the compiler can't distinguish between a ordinary const
variable and a integral constant expression with no address. In
support of my theory, making S::s non-const removes the error too.

public __gc struct S
{
static int const s = 8;
static double const d = 5.0;
};

void test()
{
int const& ref = S::s;
//&S::s;
//double const& dref = S::d;
}

I find this highly illogical. According to the compiler error message
you get when you try define S::s outside the class definition, the
declaration inside the class definition is also a definition, hence the
object exists. If the compiler decides to optimize away the storage
occupied by the static member because it is const, that's fine with me,
but then it has to take care that taking its address is okay.
Interestingly the dref line and the &S::s line won't compile, which
seems to imply that its a compiler bug - you shouldn't be able to bind
the static const int member of a __gc class to a C++ reference, but it
is letting you anyway.

[..time passes..]

I've just researched a bit about the CLR, and it appears that S::s
above is a "static literal field contract attribute", and as such,
doesn't occupy any memory and doesn't exist at runtime, only at
compile time. This is why you can't bind it to a reference, but can
use its value. Basically, you can't use it as an lvalue, only as an
rvalue. I would have thought that the compiler should flag binding to
a reference as an error though.

Standard C++ allows the binding of const references to rvalues. For
example, this is perfectly well-formed and behaves as expected:

#include <algorithm>
#include <iostream>

int main()
{
std::cout << std::max (7, 42) << std::endl;
}

Otherwise, std::max, std::min, and the greater part of the rest of the
C++ Standard Library would be of rather limited use.

Now while it wouldn't be surprising if this were yet another standard
feature disallowed in Managed C++, I don't understand why the compiler
doesn't flag this as an error in this case. Messing up the generated
code and producing undebuggable runtime exceptions is simply
inacceptable. The question is how to communicate this to the development
team since nobody except you and me seems to follow this debate.

Another question is what the canonical method of defining constants in
Managed C++ is. Can anyone please assure me that it's not #define?

Gerhard Menzl
 
T

tom_usenet

I find this highly illogical. According to the compiler error message
you get when you try define S::s outside the class definition, the
declaration inside the class definition is also a definition, hence the
object exists. If the compiler decides to optimize away the storage
occupied by the static member because it is const, that's fine with me,
but then it has to take care that taking its address is okay.

Well, the mapping of managed C++ onto the CLR seems to require that no
storage is allocated for static const members like S::s. The value of
S::s is (I think) part of the metadata for S rather than part of S
itself.
Interestingly the dref line and the &S::s line won't compile, which
seems to imply that its a compiler bug - you shouldn't be able to bind
the static const int member of a __gc class to a C++ reference, but it
is letting you anyway.

[..time passes..]

I've just researched a bit about the CLR, and it appears that S::s
above is a "static literal field contract attribute", and as such,
doesn't occupy any memory and doesn't exist at runtime, only at
compile time. This is why you can't bind it to a reference, but can
use its value. Basically, you can't use it as an lvalue, only as an
rvalue. I would have thought that the compiler should flag binding to
a reference as an error though.

Standard C++ allows the binding of const references to rvalues.

But S::s isn't an rvalue! It's a const lvalue the way it currently
works, but using it for anything other than an lvalue-to-rvalue
conversion is an error (that normally the linker would diagnose, but
managed C++ leaves it till runtime). When binding to a reference, it
binds directly without an initial conversion to an rvalue, and this is
what causes the problem.
Now while it wouldn't be surprising if this were yet another standard
feature disallowed in Managed C++, I don't understand why the compiler
doesn't flag this as an error in this case. Messing up the generated
code and producing undebuggable runtime exceptions is simply
inacceptable. The question is how to communicate this to the development
team since nobody except you and me seems to follow this debate.

I don't think the code should generate an error. The compiler should
treat static const members in managed classes as rvalues (like
literals), not lvalues as it seems to. Then it would generate correct
code, and no error. In effect, such members should be treated as
scoped #defines for the literals they are initialized with.
Another question is what the canonical method of defining constants in
Managed C++ is. Can anyone please assure me that it's not #define?

I think the way you're doing it is correct, and the compiler is wrong
(it's treating the static consts how standard C++ treats them rather
than how the CLR says it should) - perhaps its fixed in Whidbey or
Longhorn (or whatever code names the new releases have)? Anyone?

You could try e-mailing an MVP such as Carl Daniel directly...

Tom
 
G

Gerhard Menzl

tom_usenet said:
But S::s isn't an rvalue! It's a const lvalue the way it currently
works, but using it for anything other than an lvalue-to-rvalue
conversion is an error (that normally the linker would diagnose, but
managed C++ leaves it till runtime). When binding to a reference, it
binds directly without an initial conversion to an rvalue, and this is
what causes the problem.

Oh dear. My mom always told me to stay out of lvalue vs. rvalue
discussions because they can make your brain fold upon itself. Seems
like I don't have a choice now.

I am not sure I can follow you there. Binding a reference to an lvalue
is normally the least problematic case, since there is already an actual
object that the reference refers to (see 8.5.3/5). It is binding to an
*rvalue* where things start to get messy because the compiler has to
ensure that the reference does not dangle. In the case of a temporary,
it must extend the lifetime of the latter to the point where the
reference itself goes out of scope; if we have a literal, it either has
to allocate storage, or replace every occurrence of the reference with
the literal, or perform some other kind of magic that an innocent user
like me would not even dream of. This is why only const references can
be bound to rvalues. So, why should it be a problem that S::s is an
lvalue (i.e. an object), and why do you think a lvalue-to-rvalue
conversion would be required?
I don't think the code should generate an error. The compiler should
treat static const members in managed classes as rvalues (like
literals), not lvalues as it seems to. Then it would generate correct
code, and no error. In effect, such members should be treated as
scoped #defines for the literals they are initialized with.

I don't see why treating a static const member as an lvalue should cause
errors during code generation. You define a variable, you get storage
space allocated, you get an lvalue, you can take its address, in short:
binding a reference to it should be a breeze.

In Standard C++, that is. What I was trying to say is: if, for whatever
obscure reason, Managed C++ deviates from Standard C++ in this respect
and does not allow the binding of a reference to a static const member,
even if it's a const reference, then the compiler, which can clearly
diagnose this, should issue a diagnostic instead of leaving the dirty
work to the runtime.

Gerhard Menzl
 
T

tom_usenet

Oh dear. My mom always told me to stay out of lvalue vs. rvalue
discussions because they can make your brain fold upon itself. Seems
like I don't have a choice now.

I am not sure I can follow you there. Binding a reference to an lvalue
is normally the least problematic case, since there is already an actual
object that the reference refers to (see 8.5.3/5).

Well, there would be an actual object, except the CLR doesn't allocate
one (hence the error). At runtime the member S::s doesn't exist.

It is binding to an
*rvalue* where things start to get messy because the compiler has to
ensure that the reference does not dangle. In the case of a temporary,
it must extend the lifetime of the latter to the point where the
reference itself goes out of scope; if we have a literal, it either has
to allocate storage, or replace every occurrence of the reference with
the literal, or perform some other kind of magic that an innocent user
like me would not even dream of. This is why only const references can
be bound to rvalues. So, why should it be a problem that S::s is an
lvalue (i.e. an object), and why do you think a lvalue-to-rvalue
conversion would be required?

No lvalue-to-rvalue conversion is required when binding to a
reference. However, the compiler is treating S::s as an lvalue but
S::s doesn't actually exist in memory so you can surely see the
problem. If the compiler were to treat it as an rvalue (just a
straight rvalue, no lvalue-to-rvalue conversion involved), then there
wouldn't be a problem.
I don't see why treating a static const member as an lvalue should cause
errors during code generation. You define a variable, you get storage
space allocated, you get an lvalue, you can take its address, in short:
binding a reference to it should be a breeze.

Except that its undefined behaviour to treat it as an lvalue *when it
isn't defined*. static int consts in managed classes are never defined
(in the standard C++ sense).

In C++ you can have lvalues that don't exist (see my earlier example
using a static const as an array bound). Unfortunately the compiler is
treating managed class static consts as that kind of non-existent
lvalue rather than as the more logical (for managed classes) rvalue.
In Standard C++, that is. What I was trying to say is: if, for whatever
obscure reason, Managed C++ deviates from Standard C++ in this respect
and does not allow the binding of a reference to a static const member,

That's not a deviation:

struct S
{
static int const s = 5;
};

int const& ref = S::s; //link time error in normal C++
//or runtime error were S declared __gc.
//Either allowable since it is undefined behaviour.

The difference is that for a non-__gc class you can define S::s and
give it storage whilst for a __gc class there is no syntax to define
it.
even if it's a const reference, then the compiler, which can clearly
diagnose this, should issue a diagnostic instead of leaving the dirty
work to the runtime.

On the contrary, I think static consts in managed classes should work
differently to ones in normal classes - they should be rvalues rather
than lvalues. Then you won't get any compiler errors, it will just
"work", and it will fit in with the way it works in other CLR
languages.

Tom
 
G

Gerhard Menzl

tom_usenet said:
Well, there would be an actual object, except the CLR doesn't allocate
one (hence the error). At runtime the member S::s doesn't exist.

Well, then from the error you get when you do try to define S::S,

error C3366: 'S::s' : static data members of managed types must be
defined within the class definition

I would have to conclude that

1. The author of the error message text either didn't understand how
Managed C++ works or what a definition means in C++.

2. It is impossible to define a static const member in a __gc struct
or class.

Is that your understanding as well?

Gerhard Menzl
 
T

tom_usenet

Well, then from the error you get when you do try to define S::S,

error C3366: 'S::s' : static data members of managed types must be
defined within the class definition

I would have to conclude that

1. The author of the error message text either didn't understand how
Managed C++ works or what a definition means in C++.

2. It is impossible to define a static const member in a __gc struct
or class.

Is that your understanding as well?

Yes. They are treating all static members the same, rather than making
a distinction between static const "literal" ones and ordinary static
ones. Ordinary statics do have storage allocated, it's only these
const ones with the value supplied inline that cause a problem I
think.

Tom
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top