May 16th, 2023
heart6 reactions

C++20 Support Comes To C++/CLI

Tanveer Gani
Principal Software Design Engineer

We’re pleased to announce the availability of C++20 support for C++/CLIin Visual Studio 2022 v17.6. This update to MSVC was in response tofeedback received from many customers via votes onDeveloperCommunityand otherwise. Thank you!

In Visual Studio 2022 v17.6, the use of/clr /std:c++20 will nolonger cause the compiler to emit a diagnostic warning that the compilerwill implicitly downgrade to/std:c++17. Most C++20 features aresupported with the exception of the following:

  • Two-phase name lookup support formanaged templates. This istemporarily on hold pending a bugfix.
  • Support for module import under/clr.

Both the above are expected to be fixed in a near-future release ofMSVC.

The remainder of this blog post will discuss the background details,limitations, and caveats of C++20 support for C++/CLI.

Brief history and background of C++/CLI

C++/CLI was first introduced as an extension to C++98. It was specifiedas a superset of C++98 but soon with the introduction of C++11, someincompatibilities appeared between C++/CLI and ISO C++, some of whichexist to this day. [Aside:nullptr andenum class were originallyC++/CLI inventions that were migrated to ISO C++ and standardized.]With the further introduction of C++14 and C++17 standards, it becameincreasingly challenging to support the newer language standards andeventually due to the effort required to implement C++20, we decidedto temporarily limit standard support to C++17 in/clr mode. Aprime reason for this was the pause in the evolution of the C++/CLIspecification, which has not been updated since its introduction andtherefore could not guide the interaction of C++/CLI features with thenew features being introduced in ISO C++.

The design rationale for C++/CLI is spelled out inthisdocument.

Originally envisioned as a first-class language for.NET, C++/CLI’suse has primarily fallen into the category ofinterop, which includesboth directions from managed to native and vice versa. The demise ofC++/CLI has been predicted many times, but it continues to thrive inWindows applications primarily because of its strength in interop, whereit is very easy to use and hard to beat in performance. The originalgoals spelled out in the C++/CLI Rationale were:

  1. Enable C++/CLI as a first-class programming language on.NET.
  2. Use the fewest possible extensions. ISO C++ code “just works” andconforming extensions are added to ISO C++ to allow working with.NET types.
  3. Be as orthogonal as possible: if feature X works on ISO C++ types,it should also work on C++/CLI types.

With the specialization of C++/CLI as an interop language, the ECMAspecification for it was not updated to keep up with fast evolving.NET features with the result that it can no longer be called afirst-class language on.NET. While it can consume most of thefundamental types in the.NET base-class library, not all features areavailable due to lack of support from C++/CLI. This has resulted in both/clr:safe (allow only the “safe” CLS subset of CLI, disallowing nativecode) and/clr:pure (compile everything to pure MSIL code) to bedeprecated. The only options supported currently are/clr, whichtargets the.NET Framework, and/clr:netcore which targets NetCore.In both cases, compilation of native types and code, and interop aresupported.

Goal (2) was originally achieved nearly perfectly in C++98 but newerversions of ISO C++ caused MSVC to deviate from this goal.

With regard to goal (3), C++/CLI was never specified or implemented tothe required level of full generality to satisfy this goal. While somefeatures such as templates were made orthogonal to ISO C++ and managedC++/CLI types, the full generality of features such as allocatingmanaged types on the native heap withnew, allowing managed types toembed in native types, etc., were not specified by the ECMAspecification. This lack of support turns out to be fortuitous andallows us to move forward with implementing support for newer ISO C++Standards.

C++20 support for C++/CLI

While C++14 and C++17 were mostly incremental updates to C++11, as faras the core language is concerned, C++20 is a large change because offeatures likeconcepts,modules, andcoroutines. While coroutinesaren’t yet pervasive, concepts and modules are already in use in the ISOC++ Standard Library.

Generally speaking, we need support from the.NET runtime whenever thelanguage introduces a new feature which has a runtime impact in the areaofimplicit P/Invokeinterop.Two examples from C++11 are move constructors andnoexcept. The.NETruntime’s P/Invoke engine already knew how to call copy constructorswhen objects were copied across the managed/native boundary. With theintroduction of move constructors, types likestd::unique_ptr werehandled incorrectly in interop because they have a move constructorinstead of a copy constructor. Handling this correctly required addingfunctionality to the P/Invoke engine on the.NET side and generatingthe code and metadata to make sure it was called appropriately. For thenoexcept case, we still don’t have a correct implementation available.While we handlenoexcept correctly in the type system, at runtime anexception crossing a function with anoexcept specification does notresult in program termination. Implementing this would, again, requireteaching the.NET runtime how to handle such cases but due to no userdemand to handle this correctly, it has been left unimplemented.

We wanted to avoid requiring new functionality in the.NET runtimesince doing so is time consuming and requires expensive updates, so toadd support for C++20 to C++/CLI, we followed this general principle:

Separate C++/CLI types from new C++20 features but allow all possibleC++20 features with native types in a/clr compilation.

To achieve this, the implementation of C++20 support in C++/CLI followsthis scheme:

  • All native code in a translation unit is compiled to managed MSIL,with the exception of code having these constructs in it:

    1. aligned data types
    2. inline assembly
    3. calls tofunctions declared__declspec(naked)
    4. references to__ImageBase
    5. functions with vararg (...) arguments
    6. __ptr32 or__ptr64 modifiers on pointer types
    7. CPU intrinsics or other intrinsic functions
    8. virtual call thunks to virtual functions not declared__clrcall
    9. setjmp orlongjmp
    10. coroutines

    With the exception of coroutines, the above list has already beenthe case for compilation to native in prior versions of MSVC. Allsemantics conform to ISO C++ semantics, as before, the onlydifference being that the compiler emits managed instructions.Native types are emitted out to metadata as empty value classes witha given size, just to provide tokens for type names when they’reused in function signatures. Otherwise, they remain a black-box tothe runtime and are handled entirely at the byte level by thegenerated managed code.

  • Conformant (two-phase) name lookup ([temp.res] in ISO C++) isenabled innative templates in/clr compilations. Previousversions of MSVC forced the user to specify/Zc:twoPhase- whenusing/clr and any flag that implied ISO C++ name lookupsemantics.
  • Coroutines are implemented by compiling all coroutines to nativecode. This requires no new support from the.NET runtime and usesthe native runtime support. The disadvantage is that all calls tocoroutines are interop calls that have the transition andmarshalling overhead.
  • Allow concepts to interact only with native types. This is anotherviolation of the “orthogonality” goal mentioned above. The exceptionis C++/CLI types that have a 1-1 mapping with native types such asSystem::Int32, etc.
  • Allowimport of modules but not export from translation unitscompiled with/clr. In a similar vein, module header units cannotbe generated in a/clr compilation but may be used in one. Thisrestriction is because the module metadata format is based on theIFC specification, which has no support for C++/CLI types.
  • Allowall Standard Library headers to compile with/clr andC++20. Some headers had previously been blocked off from beingcompiled as managed because they included ConcRT parallelprogramming headers as a dependency, while some, like<atomic>,had no support in.NET. The dependency on ConcRT is now removedand headers previously forbidden from inclusion with/clr havebeen updated. Note: some of these headers are still forbidden frominclusion in/clr:pure mode.

No attempt is being made to fix the below pre-existing issues. If thereis user demand, these can be handled separately in the future.

  • Lack ofnoexcept support from.NET, as explained above.
  • enum class has differing meanings in C++/CLI and ISO C++. This iscurrently resolved by treating such declarations as native enumsexcept when preceded by an access specifier (as C++/CLI allows).
  • nullptr has differing meanings in C++/CLI and ISO C++. In caseswhere this matters,__nullptr is provided to mean the ISOC++nullptr value.

Going forward, we plan to use the same strategy to support future ISOC++ versions: compile constructs that have no support from.NET tonative code and keep the C++/CLI and ISO C++ type universes separate. Inthe rare case where a new mechanism is required for marshalling typesacross managed/native boundaries, we shall require new support from.NET. Historically, this has not happened since C++11.

Examples

The below examples illustrate how C++20 constructs are being handled inC++/CLI.

Coroutines

There are no restrictions on coroutines. They may be used in their fullgenerality with the understanding that the coroutines themselves arealways compiled to native code.

Consider the below program fragment:

generator<move_only> answer(){    co_yield move_only(1);    co_yield move_only(2);    co_yield move_only(3);    move_only m(4);    co_return m; // Move constructor should be used here when present}int main(){    int sum = 0;     auto g = answer();    for (move_only&& m : g)    {        sum += m.val;    }    return sum == 6 ? 0 : 42+sum;}

Inspecting the generated IL for this, we can see this IL sequence:

IL_0000:  ldc.i4.0IL_0001:  stloc.2IL_0002:  ldc.i4.0IL_0003:  stloc.0IL_0004:  ldloca.s   V_8IL_0006:  call       valuetype 'generator<move_only>'*              modreq([mscorlib]System.Runtime.CompilerServices.IsUdtReturn)              modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl)              answer(valuetype 'generator<move_only>'*)IL_000b:  pop

together with:

method assembly static pinvokeimpl(/* No map */)         valuetype 'generator<move_only>'*        modreq([mscorlib]System.Runtime.CompilerServices.IsUdtReturn)        modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl)         answer(valuetype 'generator<move_only>'* A_0)                           native unmanaged preservesig{  // Embedded native code} // end of method 'Global Functions'::answer

showing thatanswer() is a native method and the call to it in theabove MSIL disassembly fragment is an interop call. This is shown onlyfor exposition and the user has to do absolutely nothing to make itwork.

Concepts

Since concepts are a mechanism to perform computations on types atcompile time, there is no runtime component to be supported by.NET.Further, concepts “disappear” once templates are specialized andtemplates have been supported for C++/CLI from the outset. There are twokinds of templates supported in C++/CLI, ISO C++ templates andmanagedtemplates whose specialization results in managed types. We have madethe choice to keep all managed types separate from concepts and thisincludes managed templates. Any attempt to mix managed types andconcepts results in a failed compilation with diagnostics. Note thatthis excludes types likeSystem::Int32 which can be mapped directly tonative types, but boxing of such types is also excluded from interactionwith concepts.

#include <concepts>#include <utility>template<std::swappable Swappable>void Swap(Swappable& s1, Swappable& s2){    s1.swap(s2);}struct SwapMe{    int i;    void swap(SwapMe& other) { std::swap(this->i, other.i); }};value struct SwapMeV{    int i;    void swap(SwapMeV% other) { auto tmp = i; i = other.i; other.i = tmp; }};int main(){    SwapMe s1, s2;    Swap(s1, s2);    SwapMeV s1v, s2v;    Swap(s1v, s2v);   // error C7694: managed type 'SwapMeV'                       // used in a constraint definition or evaluation                      // or in an entity that uses constraints    // Boxed value types    int ^b1 = 1;          int ^b2 = 2;    Swap(b1, b2);     // error C7694: managed type 'System::Int32 ^'                      // used in a constraint definition or evaluation                      // or in an entity that uses constraints}

In the above example, the native types work exactly as for ISO C++compilation without/clr but attempting to use concepts with C++/CLItypes generates a diagnostic. It is possible to widen concepts to allowa carefully chosen subset of the C++/CLI type universe, but for thisversion of MSVC, we have chosen to keep them separate. Removing the linewith the diagnostic and inspecting the disassembly shows us

IL_0009:  ldloca.s   V_3IL_000b:  ldloca.s   V_2IL_000d:  call       voidmodopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl)  'Swap<struct SwapMe>'(    valuetype SwapMe* modopt([mscorlib]System.Runtime.CompilerServices.IsImplicitlyDereferenced),    valuetype SwapMe* modopt([mscorlib]System.Runtime.CompilerServices.IsImplicitlyDereferenced))

Note that the parameter types of the function areSwapMe* and theconceptstd::swappable does not appear.

Modules

As mentioned above, in C++/CLI modules can only be imported, either asregular ISO C++ modules, or header units, created from headers thatcontain no C++/CLI code. Thus, we have these restrictions on modules:

module m;               // error under /clrexport module m;        // error under /clrexport class X;export import m;        // error under /clrimport m;               // OK with /clrimport <vector>;        // header units OK with /clr

From the command-line, certain flag combinations will produce errors:

cl /exportHeader /clr ...       # errorcl /ifcOutput /clr ...          # errorcl /ifcOnly /clr ...            # error

Interop and module import

Since, currently we don’t allow module export under/clr, and sincethe modules can have code in associated.obj files, unless built with/ifcOnly, it follows that a call to any imported non-inline functionhas to be an interop call to native code. For templates, thisrestriction is not required and hence importing, say the class templatestd::vector, can result in its member functions being compiled tomanaged code. This is an important consideration for performance sinceinterop calls will inhibit inlining. We shall provide more guidance in afuture blog article, when support for modules under/clr ships.

Conclusion and call to action

C++20 support is being added to C++/CLI with very few restrictions asfar as native types are concerned. The general principle followed isthat of least surprise: if a particular feature is valid in a nativecompilation, there is a high chance it is also valid under/clr.

If you have C++/CLI code, we encourage you to turn on C++20 compilation,try the product and report bugs via Visual Studio Feedback. If you havespecific need for modules to be used in conjunction with C++/CLI, again,we would appreciate feedback.

Author

Tanveer Gani
Principal Software Design Engineer

Tanveer Gani is a Principal Engineer with Microsoft and has been working on C++ compilers and IDEs for over two decades.

4 comments

Discussion is closed.Login to edit/delete existing comments.

Stay informed

Get notified when new posts are published.
Follow this blog