C# versus C++, null references and protecting against them
-
What you describe is Microsoft (implementation) specific version of "undefined behavior". The code in
Foo
is ill-formed and any assumptions about what happens after you took a reference to a "null" pointer are worthless...(the lang. spec. does not require an exception to be thrown.)PS, it seems they've been arguing about this exact syntax for quite some time now. it appears there are two camps in the standard, one that wants it be defined behavior and one that does. The following like should provide some interesting reading on the subject. http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#232
-
I'm afraid you missed a subtle point in my example, the point that gets you into your function with a NULL value. I've simplified it below
void Bar(CObArray &ar)
{
int foo = ar.GetCount();
}void Foo(void)
{
CObArray *bob = NULL;Bar(\*bob);
}
set a breakpoint on the "int foo = ar.GetCount()" and observe that ar cannot be examined in the debugger, it's value is null. Note that you do in fact get to that statement, that is, the "Bar(*bob)" in the function "Foo" does not throw an exception. Although it looks like it is dereferencing the clearly null pointer, it's really just creating the argument for Bar by getting the address of the CObject and passing it, that is, making a "by reference" value for the Bar function. Therefore, the "crash" happens in "Bar" (i.e. your function) rather than at the invocation site. In my first reply, I was showing that the "ToBeAdded" object, which was valid, had a "Members" data member that was, in fact, NULL and that there was no "help" from the compiler / language rules that prevented it from passing NULL, even though the rules appear to say that's illegal. So, my conclusion was that if you don't want to allow NULL as an input to your functions and that you used to explicitly ASSERT / check for that case, you need to continue to check for that case because simply using "&" to eliminate that possibility was not going to work. Thus ends my rebuttal :)
Ouch. I stand corrected. I started the day sure the sky was blue and now I'm left wondering if their implemetation ignores the statement "must be initialized by a valid object" and the part on page 198, section 8.5.3... "...Argument passing (5.2.2) and function value return (6.6.3) are initializations." But I agree (this includes some of your statements in another path of this thread as well), that I'm stuck with their implementation. I noticed they even let me dot off the invalid reference and call methods without any exceptions. At first I was shocked, but now I'm quite intrigued to see if any compiler vendor can implement this correctly. I'm glad I never had to go up against you in debate class. :) Excuse me while I go fix a million lines of active code. ;P
-
Ouch. I stand corrected. I started the day sure the sky was blue and now I'm left wondering if their implemetation ignores the statement "must be initialized by a valid object" and the part on page 198, section 8.5.3... "...Argument passing (5.2.2) and function value return (6.6.3) are initializations." But I agree (this includes some of your statements in another path of this thread as well), that I'm stuck with their implementation. I noticed they even let me dot off the invalid reference and call methods without any exceptions. At first I was shocked, but now I'm quite intrigued to see if any compiler vendor can implement this correctly. I'm glad I never had to go up against you in debate class. :) Excuse me while I go fix a million lines of active code. ;P
Hm, not sure to whom this may concern, I always get confused by these hierarchies of threads... Anyway, the concept of a C++ reference is a subtle thing and it got even subtler with c++0x, as you might know. For good reasons this concept wasn't considered is what C# and other CLR targeting languages - and now we're back at OP's problem (at least as I see it...): in case of reference types (and in a verifiable program), you always deal with handles ("pointers") to instances. These handles can have two states: (a) pointing to valid object, (b) or "null". You can take a value of a "handle" by reference, by stating this intent explicitely at both call and target sites. However, this does not change the semantics of a handle, you just get access to the location of whatever the handle represents. So in C#, if you want to protect against handle pointing to "null", you either have to be explicit about that (code contracts are a great way to do that), or you rely on NullArgumentException handling (which is guaranteed to be thrown by the specs). The
ref
modifier mentioned somewhere in the thread does not help much:void Bar(MyClass pParam) {
pParam.Member(); // guaranteed NullArgument exception if null
}void Bar(ref MyClass pParam) {
pParam.Member(); // guaranteed NullArgument exception if null
pParam = new MyClass();
}void Foo() {
MyClass tI1;
MyClass tI2 = null;
MyClass tI3 = new MyClass();Bar(ref tI1); // error, tI1 uninitialized Bar(ref tI2); // ok Bar(ref tI3); // ok Bar(tI1); // warning or error, not sure Bar(tI2); // NullArgumentEx... Bar(tI3); // ok
}