Thursday, February 7, 2008

Microsoft VS2005 C++ non-compliance issues (Part II)

2. Issues that respond to /Za

Header files supplied with VS2005 seem to tolerate /Za setting (although, as I said before, I haven't checked all of them). For this reason, the issues listed in this section are not as critical as the previous ones. Most will probably see them as compiler extensions, which can be easily disabled if necessary. Yet, if for some reason you can't use /Za then these are something to be aware of.

Since the /Za switch is supposed to control language extensions, one might ask about the difference between the extensions listed in this section (part II) and extensions listed in the next one (part III). Well, by definition, a true "language extension" takes place when the compiler takes specific steps to define the behavior of a program, whose behavior would otherwise be undefined by the language specification (that includes ill-formed code). In other words, extensions allow the compiler to treat an invalid C++ program as a valid one, interpreting it in some implementation-defined fashion. However, one key moment here is that the compiler is never allowed to change the behavior of what is originally a valid C++ program. Unfortunately, in a number of cases VS2005 compiler does change the behavior of a valid C++ program to the point when it no longer agrees with the language specification. Even though these issues can be controlled with /Za switch, they still aren't "language extensions". This part is specifically intended to include the issues of this particular kind, while part III is intended to be used for true language extensions.

The text below describes the compiler behavior with language extensions enabled (i.e. /Za is not used).

2.1. Implicit conversion of function pointers to 'void*'

VS2005 assumes that function pointers are implicitly convertible to 'void*' type. For example, in the following code sample

void foo(); ... std::cout << &foo << std::endl;

overload resolution will select the version of '<<' operator that outputs 'const void*' values. A compliant compiler shall select the version for 'bool' values in this case.

Additionally, during overload resolution VS2005 believes that implicit conversion of a function pointer to 'void*' is a better alternative than matching that function pointer to ellipsis parameter specification, as illustrated by the following example

void foo(); void bar(void*); void bar(...); ... bar(foo); // 'bar(void*)' is called here, while 'bar(...)' is // supposed to be called by a compliant compiler

This behavior might lead to incorrect results in some known template meta-programming tricks and techniques.

Once again, VS2005 seems to behave correctly in this respect when used with /Za switch.

2.2. Multiple user-defined conversions are allowed in copy-initialization

This issue is best illustrated by the following code sample

struct A { A(int); }; struct B { B(const A&); }; ... // Direct-initialization, B bd(0); // OK // Copy-initialization B bc = 0; // ill-formed, but OK in VS2005

The initialization of 'bc' is a copy-initialization and the types involved on the left-hand side ('B') and the right-hand size ('int') are not the same. In this case the initialization must attempt to convert type 'int' to type 'B' by selecting one of the 'B's conversion constructors and then copy the result of the conversion to 'bc' by using 'B's copy constructor. This process in not allowed to perform any additional intermediate conversions, like convert 'int' to 'A' and then convert 'A' to 'B'. Yet that's exactly what VS2005 does.

One side effect of this behavior is that the following initialization compiles fine in VS2005

std::auto_ptr<int> p = new int;

while virtually everyone who ever worked with 'std::auto_ptr' knows for a fact that this code is supposed to be ill-formed. Prohibiting this initialization is actually the reason why the pointer-to-auto_ptr conversion constructor is declared 'explicit'. Standard library, which comes with VS2005 also declares this constructor 'explicit', but its effect is immediately defeated by the issue in question: VS2005 happily works around the restriction by performing a dual user defined conversion. Firstly, it converts the pointer to 'auto_ptr_ref', and then it converts the resultant 'auto_ptr_ref' to 'auto_ptr'. (Why 'auto_ptr_ref' is so immediately available to user code is another question.)

The first thing that comes to mind is that VS2005 simply handles copy-initialization in the same manner as direct-initialization.

One interesting detail about this extended behavior is that it applies to the implicit copy-initialization in function return, but doesn't apply to function argument passing. Function arguments are initialized by copy-initialization and that particular copy-initialization works correctly, meaning that it is restricted to just one user-defined conversion. For example, in the context of the first code sample the following code will not compile in VS2005

B foo(B){ return 0; // ill-formed, but OK in VS2005 } ... foo(0); // ill-formed, rejected by VS2005

Most likely the argument passing context was singled out and left unextended in order to avoid breaking some known overloading-based template meta-programming techniques, which otherwise would become useless in VS2005. Under these circumstances it is difficult to say whether this behavior of VS2005 is a relatively harmless language extension or a serious code-breaking non-compliance issue. For now I'll leave it in this part of the report.

2.3. Non-constant reference can be bound to a temporary

This is an old and a well-known issue with VC6, which could be fixed with /Za switch in VC6, just like it can be fixed with that switch in VS2005.

struct S {}; ... S& r = S(); // ill-formed, but OK in VC6 and VS2005

However, there is a number of interesting changes in VS2005, which generally make this issue not as harmful as it was in VC6.

In VC6 the compiler used to carelessly bind non-constant references to temporary objects without even giving it a second thought. For example, given a choice of constant and non-constant reference during overload resolution VC6 blindly selected the latter

struct S {}; void foo(S&); void foo(const S&); ... foo(S()); // VC6 resolves it to the 'foo(S&)' version

VS2005 appears to follow a different logic. During overload resolution it still considers functions having non-constant reference parameters for temporary arguments as viable functions. However, the conversion sequence necessary to perform the binding is given the lowest rank. This generally means that VS2005 tries to perform the overload resolution as close to the standard requirements as possible, and only when if the standard resolution fails does it consider the non-standard binding as the last resort. In accordance with this approach in the previous code sample VS2005 will correctly select the 'foo(const S&)' version of overloaded function. In the next sample VS2005 also exhibits standard-compliant behavior

struct S { operator int() const; }; void foo(S&); void foo(int); ... foo(S()); // VS2005 resolves it to the 'foo(int)' version

No comments: