Diagnostic Improvements in Visual Studio 2017 15.3.0

This post as well as described diagnostics significantly benefited from the feedback by Mark, Xiang, Stephan, Marian, Gabriel, Ulzii, Steve and Andrew.

Visual Studio 2017 15.3.0 release comes with a number of improvements to the Microsoft Visual C++ compiler’s diagnostics. Most of these improvements are in response to the diagnostics improvements survey we shared with you at the beginning of the 15.3 development cycle. Below you will find some of the new or improved diagnostic messages in the areas of member initialization, enumerations, dealing with precompiled headers, conditionals, and more. We will continue this work throughout VS 2017.

Order of Members Initialization

A constructor won’t initialize members in the order their initializers are listed in code, but in the order the members are declared in class. There are multiple potential problems that can stem from assuming that actual initialization order will match the code. A typical scenario is when the member initialized earlier in the list is used in a later initializer, while the actual initialization happens in the reverse order because of the order of declaration of those members.

// Compile with /w15038 to enable the warning
struct B : A
{
    B(int n) : b(n), A(b) {} // warning C5038: data member 'B::b' will be initialized after base class 'A'
    int b;
};

The above warning is off-by-default in the current release due to the amount of code it breaks in numerous projects that treat warnings as errors. We plan to enable the warning by default in a subsequent release, so we recommend to try enabling it early.

Constant Conditionals

There were a few suggestions to adopt the practice popularized by Clang of suppressing certain warnings when extra parentheses are used. We looked at it in the context of one bug report suggesting that we should suppress "warning C4127: conditional expression is constant" when the user puts extra () (note that Clang itself doesn’t apply the practice to this case). While we discussed the possibility, we decided this would be a disservice to good programming practices in the context of this warning as the language and our implementation now supports the ‘if constexpr’ statement. Instead, we now recommend using ‘if constexpr’.

    if ((sizeof(T) < sizeof(U))) …
        // warning C4127 : conditional expression is constant
        // note : consider using 'if constexpr' statement instead 

Scoped Enumerations

One reason scoped enumerations (aka enum classes) are preferred is because they have stricter type-checking rules than unscoped enumerations and thus provide better type safety.  We were breaking that type safety in switch statements by allowing developers to accidently mix enumeration types. This often resulted in unexpected runtime behavior:

enum class A { a1, a2 };
enum class B { baz, foo, a2 };
int f(A a) {
    switch (a)
    {
    case B::baz: return 1;
    case B::a2:  return 2;
    }
    return 0;
}

In /permissive- mode (again, due to the amount of code this broke) we now emit errors:

error C2440: 'type cast': cannot convert from 'int' to 'A'
note: Conversion to enumeration type requires an explicit cast (static_cast, C-style cast or function-style cast)
error C2046: illegal case

The error will also be emitted on pointer conversions in a switch statement.

Empty Declarations

We used to ignore empty declarations without any diagnostics, assuming they were pretty harmless. Then we came across a couple of usage examples where users used empty declarations on templates in some complicated template-metaprogramming code with the assumption that those would lead to instantiations of the type of the empty declaration. This was never the case and thus was worth notifying about. In this update we reused the warning that was already happening in similar contexts, but in the next update we’ll change it to its own warning.

struct A { … };
A; // warning C4091 : '' : ignored on left of 'A' when no variable is declared

Precompiled Headers

We had a number of issues in large projects arising from the use of precompiled headers on very large projects. The issues weren’t compiler-specific per se, but rather dependent on processes happening in the operating system. Unfortunately, our one error fits all for this scenario was inadequate for the users to troubleshoot the problem and come up with a suitable workaround. We expanded the information that the errors contained in these cases in order to be better able to identify a specific scenario that could have led to the error and advise users on the ways to address the issue.

error C3859: virtual memory range for PCH exceeded; please recompile with a command line option of '-Zm13' or greater
note: PCH: Unable to get the requested block of memory
note: System returned code 1455: The paging file is too small for this operation to complete
note: please visit https://aka.ms/pch-help for more details
fatal error C1076: compiler limit: internal heap limit reached; use /Zm to specify a higher limit

The broader issue is discussed in greater details in our earlier blog post: Precompiled Header (PCH) issues and recommendations

Conditional Operator

The last group of new diagnostic messages are all related to our improvements to the conformance of the conditional operator ?:. These changes are also opt-in and are guarded by the switch /Zc:ternary (implied by /permissive-) due to the amount of code they broke. In particular, the compiler used to accept arguments in the conditional operator ?: that are considered ambiguous by the standard (see section [expr.cond]). We no longer accept them under /Zc:ternary or /permissive- and you might see new errors appearing in source code that compiles clean without these flags.

The typical code pattern this change breaks is when some class U both provides a constructor from another type T and a conversion operator to type T (both non-explicit). In this case both the conversion of the 2nd argument to the type of the 3rd and the conversion of the 3rd argument to the type of the 2nd are valid conversions, which is ambiguous according to the standard.

struct A
{
	A(int);
	operator int() const;
};

A a(42);
auto x = cond ? 7 : a; // A: old permissive behavior prefers A(7) over (int)a. 
                       // The non-permissive behavior issues:
                       //     error C2445: result type of conditional expression is ambiguous: types 'int' and 'A' can be converted to multiple common types
                       //     note: could be 'int'
                       //     note: or       'A'

To fix the code, simply cast one of the arguments explicitly to the type of the other.

There is one important exception to this common pattern when T represents one of the null-terminated string types (e.g. const char*, const char16_t* etc., but you can also reproduce this with array types and the pointer types they decay to) and the actual argument to ?: is a string literal of corresponding type. C++17 has changed the wording, which led to change in semantics from C++14 (see CWG defect 1805). As a result, the code in the following example is accepted under /std:c++14 and rejected under /std:c++17:

struct MyString
{
	MyString(const char* s = "") noexcept; // from const char*
	operator const char*() const noexcept; //   to const char*
};
MyString s;
auto x = cond ? "A" : s; // MyString: permissive behavior prefers MyString("A") over (const char*)s

The fix is again to cast one of the arguments explicitly.

In the original example that triggered our conditional operator conformance work, we were giving an error where the user was not expecting it, without describing why we give an error:

auto p1 = [](int a, int b) { return a > b; };
auto p2 = [](int a, int b) { return a > b; };
auto p3 = x ? p1 : p2; // This line used to emit an obscure error:
error C2446: ':': no conversion from 'foo::<lambda_f6cd18702c42f6cd636bfee362b37033>' to 'foo::<lambda_717fca3fc65510deea10bc47e2b06be4>'
note: No user-defined-conversion operator available that can perform this conversion, or the operator cannot be called

With /Zc:ternary the reason for failure becomes clear even though some people might still not like that we chose not to give preference to any particular (implementation-defined) calling convention on architectures where we support multiple:

error C2593: 'operator ?' is ambiguous
note: could be 'built-in C++ operator?(bool (__cdecl *)(int,int), bool (__cdecl *)(int,int))'
note: or       'built-in C++ operator?(bool (__stdcall *)(int,int), bool (__stdcall *)(int,int))'
note: or       'built-in C++ operator?(bool (__fastcall *)(int,int), bool (__fastcall *)(int,int))'
note: or       'built-in C++ operator?(bool (__vectorcall *)(int,int), bool (__vectorcall *)(int,int))'
note: while trying to match the argument list '(foo::<lambda_717fca3fc65510deea10bc47e2b06be4>, foo::<lambda_f6cd18702c42f6cd636bfee362b37033>)'

Another scenario where one would encounter errors under /Zc:ternary are conditional operators with only one of the arguments being of type void (while the other is not a throw expression). A common use of these in our experience of fixing the source code this change broke was in ASSERT-like macros:

void myassert(const char* text, const char* file, int line);
#define ASSERT(ex) (void)((ex) ? 0 : myassert(#ex, __FILE__, __LINE__))
 
error C3447: third operand to the conditional operator ?: is of type 'void', but the second operand is neither a throw-expression nor of type 'void'

The typical solution is to simply replace the non-void argument with void().

A bigger source of problems related to /Zc:ternary might be coming from the use of the conditional operator in template meta-programming as some of the result types would change under this switch. The following example demonstrates change of conditional expression’s result type in a non-meta-programming context:

      char  a = 'A';
const char  b = 'B';
decltype(auto) x = cond ? a : b; // char without, const char& with /Zc:ternary
const char(&z)[2] = argc > 3 ? "A" : "B"; // const char* without /Zc:ternary

The typical resolution in such cases would be to apply a std::remove_reference trait on top of the result type where needed in order to preserve the old behavior.

In Closing

You can try these improvements today by downloading Visual Studio 2017 15.3.0 Preview. As always, we welcome your feedback – it helps us prioritize our work as well as the rest of the community is resolving similar issues. Feel free to send any comments through e-mail at visualcpp@microsoft.com, Twitter @visualc, or Facebook at Microsoft Visual Cpp. If you haven’t done so yet, please check also our previous post in the series documenting our progress on improving compiler diagnostics.

If you encounter other problems with MSVC in VS 2017 please let us know via the Report a Problem option, either from the installer or the Visual Studio IDE itself. For suggestions, let us know through UserVoice.

Thank you!
Yuriy