Better template support and error detection in C++ Modules with MSVC 2017 version 15.9

Overview

It has been a long time since we last talked about C++ Modules. We feel it is time to revisit what has been happening under the hood of MSVC for modules.

The Visual C++ Team has been dedicated to pushing conformance to the standard with a focus on making the overall compiler implementation more robust and correct with the rejuvenation effort. This rejuvenation effort has given us the ability to substantially improve our modules implementation. We’ve mostly done this work transparently over the past few months until now. We are proud to say the work has reached a point where talking about it would hopefully provide developers with even more reasons to use C++ Modules with MSVC!

What is new?

  • Two-phase name lookup is now a requirement to use modules. We now store templates in a form that is far more structured than the previous token stream model preserving information like bound names at the point of declaration.
  • Better constexpr support allows users to write far more complex code in an exported module.
  • Improved diagnostics provide users with more safety and correctness when using modules with MSVC.

Two-Phase Name Lookup Support

This point is best illustrated through example. Let’s take the VS2017 15.7 compiler and build some modules. Given a module m.ixx:

#include <type_traits>
export module m;

export
template <typename T>
struct ptr_holder {
  static_assert(std::is_same_v<T, std::remove_pointer_t<T>>);
};

Now, let’s use the module in a very simple program to try and trigger the static_assert to fail, main.cpp:

import m;

int main() {
  ptr_holder<char*> p;
}

Using the command line, we can build this module and program like so:

cl /experimental:module /std:c++17 /c m.ixx
cl /experimental:module /std:c++17 /module:reference m.ifc main.cpp m.obj

However, you will quickly find this results in a failure:

m.ixx(7): error C2039: 'is_same_v': is not a member of 'std'
predefined C++ types (compiler internal)(238): note: see declaration of 'std'
main.cpp(4): note: see reference to class template instantiation 'ptr_holder' being compiled
m.ixx(7): error C2065: 'is_same_v': undeclared identifier
m.ixx(7): error C2275: 'T': illegal use of this type as an expression
m.ixx(7): error C2039: 'remove_pointer_t': is not a member of 'std'
predefined C++ types (compiler internal)(238): note: see declaration of 'std'
m.ixx(7): error C2061: syntax error: identifier 'remove_pointer_t'
m.ixx(7): error C2238: unexpected token(s) preceding ';'
main.cpp(35): fatal error C1903: unable to recover from previous error(s); stopping compilation
INTERNAL COMPILER ERROR in 'cl.exe'
    Please choose the Technical Support command on the Visual C++
    Help menu, or open the Technical Support help file for more information

It failed to compile, just not in the way we expected. It appears as though the compiler did not handle this scenario. The good news is that 15.9 is here and it is coming with some much-needed improvement! Let’s build this module and program with the 15.9 compiler:

main.cpp(4): error C2607: static assertion failed
main.cpp(4): note: see reference to class template instantiation 'ptr_holder' being compiled

This! This is what we are looking for! So what gives here? Why is 15.9 able to handle this scenario while the 15.7 compiler fails in the way that it does? It all comes down to how modules works with two-phase name lookup.

As mentioned in our two-phase name lookup blog, templates were historically stored in the compiler as streams of tokens which does not save information about what identifiers were seen during the parsing of that template declaration.

The 15.7 modules implementation did not have any awareness of two-phase name lookup so template code compiled with it would suffer from many of the same problems that are described in that blog along with the by-design lookup hiding nature of non-exported module code (in our case is_same_v was a non-exported declaration).

Due to MSVC now supporting two-phase name lookup, our modules implementation is now able to handle much more complex and correct code!

Better Constexpr Support

Constexpr is something that is very important to C++ now and having support for it in combination with new language features can dramatically impact usability of that feature. As such we have made some significant improvements to the constexpr handling of our modules implementation. Once again, let us start with a concrete example, given a module m.ixx:

export module m;

struct internal { int value = 42; };

export {

struct S {
  static constexpr internal value = { };
  union U {
    int a;
    double b;
    constexpr U(int a) : a{ a } { }
    constexpr U(double b) : b{ b } { }
  };
  U u = { 1. };
  U u2 = { 1 };
};
constexpr S s;
constexpr S a[2] = { {}, {.2, 2} };

}

Using the module in a program, main.cpp:

import m;

int main() {
  static_assert(S::value.value == 42);
  static_assert(s.u.b == 1. && s.u2.a == 1);
  static_assert(a[1].u.b == .2 && a[1].u2.a == 2);
  return s.u2.a + a[1].u2.a;
}

You will identify another problem:

main.cpp(5): error C3865: '__thiscall': can only be used on native member functions
main.cpp(5): error C2028: struct/union member must be inside a struct/union
main.cpp(5): fatal error C1903: unable to recover from previous error(s); stopping compilation
Internal Compiler Error in cl.exe.  You will be prompted to send an error report to Microsoft later.
INTERNAL COMPILER ERROR in 'cl.exe'
    Please choose the Technical Support command on the Visual C++
    Help menu, or open the Technical Support help file for more information

Well those are some cryptic errors… One might discover that once you rewrite “constexpr S s;” in the form of “constexpr S s = { };” that the errors go away and you then face a new runtime failure in that the return value from main is 0, rather than the expected 3. In general, prior 15.9 constexpr objects and arrays have been a source of numerous bugs in modules. Failures such as the ones mentioned above are completely gone due in part to our recent rework of the constexpr implementation.

Improved Diagnostics

The MSVC C++ Modules implementation is not just about exporting/importing code correctly, but also providing a safe and user-friendly experience around it.

One such feature is the ability for the compiler to diagnose if the module interface unit has been tampered with. Let us see a simple example:

C:\> cl /experimental:module /std:c++latest /c m.ixx
m.ixx
C:\> echo 1 >> "m.ifc"
C:\> cl /experimental:module /std:c++latest main.cpp
main.cpp
main.cpp(1): error C7536: ifc failed integrity checks. Expected SHA2: '66d5c8154df0c71d4cab7665bab4a125c7ce5cb9a401a4d8b461b706ddd771c6'

Here the compiler refuses to try and use an interface file which has failed a basic integrity check. This protects users from malicious interface files attempting to be processed by MSVC.

Another usability feature we have added is the capability to issue warnings whenever compiler flags differ from a built module to the compiler flags used to import that module. Having some command line switches omitted from the import side could produce an erroneous scenario:

C:\> cl /experimental:module /std:c++17 /MDd /c m.ixx
m.ixx
C:\> cl /experimental:module /std:c++14 /MD main.cpp
main.cpp
main.cpp(1): warning C5050: Possible incompatible environment while importing module 'm': _DEBUG is defined in module command line and not in current command line
main.cpp(1): warning C5050: Possible incompatible environment while importing module 'm': mismatched C++ versions. Current "201402" module version "201703"

The compiler is telling us that the macro “_DEBUG” was defined when the module was built, that macro is implicitly defined when using the /MDd switch. The presence of this macro can affect how libraries like the STL behave; it can even affect their binary interface (ABI). Additionally, the standard C++ versions between these two components don’t agree so a warning is produced to inform the user.

What now (call to action)?

Download Visual Studio 2017 Version 15.9 today and try out C++ Modules with your projects. Export your template metaprogramming libraries and truly hide your implementation details behind non-exported regions! No longer fear exporting constexpr objects from your interface units! Finally, enjoy using modules with better diagnostics support to create a much more user-friendly experience!

What’s next…

  • Modules standardization is in progress: Currently, MSVC supports all of the features of the current TS. As the C++ Modules TS evolves, we will continue to update our implementation and feature set to reflect the new proposal. Breaking changes will be documented as per usual via the release notes.
  • Throughput is improving: One consequence of using old infrastructure with new infrastructure is newer code often depends on older behavior. MSVC is no exception here. We are constantly updating the compiler to be faster and rely less on outdated routines and as this begins to happen our modules implementation will speedup as a nice side-effect. Stay tuned for a future blog on this.

As always, we welcome your feedback. Feel free to send any comments through e-mail at visualcpp@microsoft.com, through Twitter @visualc, or Facebook at Microsoft Visual Cpp.

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 DevComm. Thank you!