Microsoft annonce la prise en charge de C++20 pour C++/CLI dans Visual Studio 2022 v17.6, le support impose très peu de restrictions concernant les types natifs

Microsoft a le plaisir d'annoncer la disponibilité de la prise en charge de C++20 pour C++/CLI dans Visual Studio 2022 v17.6. Cette mise à jour de MSVC a été réalisée en réponse aux commentaires reçus de nombreux clients par le biais de votes sur la communauté des développeurs et autres.

Nom : c++20.jpg
Affichages : 22721
Taille : 18,7 Ko

Dans Visual Studio 2022 v17.6, l'utilisation de /clr /std:c++20 n'entraînera plus l'émission d'un avertissement de diagnostic indiquant que le compilateur rétrogradera implicitement vers /std:c++17. La plupart des fonctionnalités du C++20 sont supportées à l'exception des suivantes :

  • Prise en charge de la recherche de nom en deux phases pour les modèles gérés. Cette fonctionnalité est temporairement en attente d'une correction de bogue.
  • Prise en charge de l'importation de modules sous /clr.

Ces deux points devraient être corrigés dans une prochaine version de MSVC.

Le reste de ce billet de blog traite des détails de fond, des limitations et des mises en garde concernant la prise en charge de C++20 pour C++/CLI.

Bref historique de C++/CLI

C++/CLI a été introduit pour la première fois en tant qu'extension de C++98. Il a été spécifié comme un surensemble du C++98, mais avec l'introduction de C++11, certaines incompatibilités sont apparues entre C++/CLI et ISO C++, dont certaines subsistent encore aujourd'hui. [A titre d'exemple, nullptr et enum class étaient à l'origine des inventions de C++/CLI qui ont été transférées à ISO C++ et normalisées]. Avec l'introduction des standards C++14 et C++17, il est devenu de plus en plus difficile de supporter les nouveaux standards du langage et finalement, en raison de l'effort requis pour implémenter C++20, nous avons décidé de limiter temporairement le support des standards à C++17 en mode /clr. L'une des principales raisons de cette décision est la pause dans l'évolution de la spécification C++/CLI, qui n'a pas été mise à jour depuis son introduction et n'a donc pas pu guider l'interaction des fonctionnalités C++/CLI avec les nouvelles fonctionnalités introduites dans l'ISO C++.

La raison d'être de C++/CLI est exposée dans ce document.

Conçu à l'origine comme un langage de première classe pour .NET, l'utilisation de C++/CLI est principalement tombée dans la catégorie de l'interopérabilité, qui comprend les deux directions, de la gestion vers le natif et vice versa. La fin de C++/CLI a été prédite à de nombreuses reprises, mais il continue de prospérer dans les applications Windows principalement en raison de sa force dans l'interopérabilité, où il est très facile à utiliser et difficile à battre en termes de performances. Les objectifs initiaux énoncés dans la justification de C++/CLI étaient les suivants :

  • Faire de C++/CLI un langage de programmation de premier ordre sur .NET.
  • Utiliser le moins d'extensions possible. Le code ISO C++ "fonctionne simplement" et des extensions conformes sont ajoutées à ISO C++ pour permettre de travailler avec les types .NET.
  • Être aussi orthogonal que possible : si une fonctionnalité X fonctionne avec les types ISO C++, elle devrait également fonctionner avec les types C++/CLI.

Avec la spécialisation du C++/CLI en tant que langage interopérable, la spécification de l'ECMA pour ce langage n'a pas été mise à jour pour suivre l'évolution rapide des fonctionnalités de .NET, ce qui fait qu'il ne peut plus être considéré comme un langage de première classe sur .NET. Bien qu'il puisse utiliser la plupart des types fondamentaux de la bibliothèque de classe de base de .NET, toutes les fonctionnalités ne sont pas disponibles en raison de l'absence de prise en charge par C++/CLI. C'est pourquoi les options /clr:safe (qui n'autorise que le sous-ensemble CLS "sûr" de CLI, en interdisant le code natif) et /clr:pure (qui compile tout en code MSIL pur) ont été dépréciées. Les seules options supportées actuellement sont /clr, qui cible le .NET Framework, et /clr:netcore qui cible NetCore. Dans les deux cas, la compilation des types et du code natifs et l'interopérabilité sont prises en charge.

L'objectif (2) était à l'origine presque parfaitement atteint dans C++98, mais les nouvelles versions d'ISO C++ ont amené MSVC à s'écarter de cet objectif.

En ce qui concerne l'objectif (3), C++/CLI n'a jamais été spécifié ou mis en œuvre au niveau de généralité requis pour atteindre cet objectif. Alors que certaines caractéristiques telles que les modèles ont été rendues orthogonales aux types ISO C++ et aux types C++/CLI gérés, la généralité complète de caractéristiques telles que l'allocation de types gérés sur le tas natif avec new, permettant aux types gérés de s'intégrer dans les types natifs, etc. n'a pas été spécifiée par la spécification de l'ECMA. Cette absence de prise en charge s'est avérée fortuite et nous a permis d'avancer dans la mise en œuvre de la prise en charge des normes ISO C++ les plus récentes.

Prise en charge de C++20 pour C++/CLI

Alors que C++14 et C++17 étaient principalement des mises à jour incrémentales de C++11, en ce qui concerne le cœur du langage, C++20 représente un changement important grâce à des fonctionnalités telles que les concepts, les modules et les coroutines. Si les coroutines ne sont pas encore omniprésentes, les concepts et les modules sont déjà utilisés dans la bibliothèque standard ISO C++.

D'une manière générale, nous avons besoin du soutien du runtime .NET chaque fois que le langage introduit une nouvelle fonctionnalité qui a un impact sur le runtime dans le domaine de l'interopérabilité implicite P/Invoke. Deux exemples tirés de C++11 sont les constructeurs move et noexcept. Le moteur P/Invoke du moteur d'exécution .NET savait déjà comment appeler les constructeurs de copie lorsque les objets étaient copiés à travers la frontière gérée/native. Avec l'introduction des constructeurs de déplacement, des types comme std::unique_ptr ont été traités de manière incorrecte dans l'interopérabilité parce qu'ils ont un constructeur de déplacement au lieu d'un constructeur de copie. Pour gérer cela correctement, il a fallu ajouter une fonctionnalité au moteur P/Invoke du côté de .NET et générer le code et les métadonnées pour s'assurer qu'elle était appelée de manière appropriée. Pour le cas noexcept, nous ne disposons toujours pas d'une implémentation correcte. Bien que nous gérions correctement le cas noexcept dans le système de types, au moment de l'exécution, une exception traversant une fonction avec une spécification noexcept n'entraîne pas l'arrêt du programme. L'implémentation de cette fonctionnalité nécessiterait, une fois de plus, d'apprendre au moteur d'exécution .NET à gérer de tels cas, mais en l'absence de demande de la part des utilisateurs, cette fonctionnalité n'a pas été implémentée.

Nous voulions éviter d'exiger de nouvelles fonctionnalités dans le moteur d'exécution .NET, car cela prend du temps et nécessite des mises à jour coûteuses, aussi avons-nous suivi ce principe général pour ajouter la prise en charge de C++20 à C++/CLI :

Séparer les types C++/CLI des nouvelles fonctionnalités C++20, mais autoriser toutes les fonctionnalités C++20 possibles avec des types natifs dans une compilation /clr.

Pour ce faire, l'implémentation du support C++20 dans C++/CLI suit le schéma suivant :

  • Tout le code natif d'une unité de traduction est compilé en MSIL géré, à l'exception du code contenant ces constructions :
    1. types de données alignés
    2. assemblage inline
    3. appels à des fonctions déclarées __declspec(naked)
    4. références à __ImageBase
    5. fonctions avec des arguments vararg (...)
    6. modificateurs __ptr32 ou __ptr64 sur les types de pointeurs
    7. fonctions intrinsèques de l'unité centrale ou autres fonctions intrinsèques
    8. appels virtuels à des fonctions virtuelles non déclarées __clrcall
    9. setjmp ou longjmp
    10. coroutines
  • À l'exception des coroutines, la liste ci-dessus était déjà valable pour la compilation en mode natif dans les versions antérieures de MSVC. Toute la sémantique est conforme à la sémantique ISO C++, comme auparavant, la seule différence étant que le compilateur émet des instructions gérées. Les types natifs sont transmis aux métadonnées sous la forme de classes de valeurs vides d'une taille donnée, afin de fournir des jetons pour les noms de types lorsqu'ils sont utilisés dans les signatures de fonctions. Sinon, ils restent une boîte noire pour le runtime et sont entièrement gérés au niveau de l'octet par le code géré généré.
  • La recherche de nom conforme (en deux phases) ([temp.res] dans ISO C++) est activée dans les modèles natifs dans les compilations /clr. Les versions précédentes de MSVC obligeaient l'utilisateur à spécifier /Zc:twoPhase- lors de l'utilisation de /clr et de tout flag qui impliquait une sémantique de recherche de nom ISO C++.
  • Les coroutines sont implémentées en compilant toutes les coroutines en code natif. Cela ne nécessite aucun nouveau support de la part du runtime .NET et utilise le support du runtime natif. L'inconvénient est que tous les appels aux coroutines sont des appels interop qui ont des surcharges de transition et de marshalling.
  • Autoriser les concepts à interagir uniquement avec les types natifs. Il s'agit d'une autre violation de l'objectif d'"orthogonalité" mentionné ci-dessus. L'exception concerne les types C++/CLI qui ont une correspondance 1-1 avec les types natifs tels que System::Int32, etc.
  • Autoriser l'importation de modules mais pas l'exportation à partir d'unités de traduction compilées avec /clr. Dans le même ordre d'idées, les unités d'en-tête de module ne peuvent pas être générées lors d'une compilation /clr, mais peuvent être utilisées dans une telle compilation. Cette restriction est due au fait que le format des métadonnées des modules est basé sur la spécification IFC, qui ne prend pas en charge les types C++/CLI.
  • Permettre à tous les en-têtes de la bibliothèque standard de compiler avec /clr et C++20. Certains en-têtes étaient auparavant bloqués pour être compilés en tant que gérés parce qu'ils incluaient les en-têtes de programmation parallèle ConcRT en tant que dépendance, tandis que certains, comme <atomic>, n'avaient pas de support dans .NET. La dépendance sur ConcRT est maintenant supprimée et les en-têtes précédemment interdits d'inclusion avec /clr ont été mis à jour. Note : certains de ces en-têtes sont toujours interdits d'inclusion dans le mode /clr:pure.

Aucune tentative n'est faite pour résoudre les problèmes préexistants ci-dessous. Si les utilisateurs le souhaitent, ces problèmes pourront être traités séparément à l'avenir.

  • Absence de prise en charge de noexcept par .NET, comme expliqué ci-dessus.
  • enum class a des significations différentes en C++/CLI et ISO C++. Ce problème est actuellement résolu en traitant ces déclarations comme des enums natifs, sauf lorsqu'elles sont précédées d'un spécificateur d'accès (comme le permet C++/CLI).
  • nullptr a des significations différentes en C++/CLI et en ISO C++. Dans les cas où cela est important, __nullptr est fourni pour signifier la valeur nullptr de l'ISO C++.

À l'avenir, nous prévoyons d'utiliser la même stratégie pour prendre en charge les futures versions de l'ISO C++ : compiler les constructions qui ne sont pas prises en charge par .NET en code natif et séparer les univers de types C++/CLI et ISO C++. Dans les rares cas où un nouveau mécanisme est nécessaire pour le marshalling des types à travers les frontières gérées/natives, nous aurons besoin d'un nouveau support de la part de .NET. Historiquement, cela ne s'est pas produit depuis C++11.

Exemples

Les exemples ci-dessous illustrent la manière dont les constructions C++20 sont gérées dans C++/CLI.

Coroutines

Les coroutines ne font l'objet d'aucune restriction. Elles peuvent être utilisées dans toute leur généralité, étant entendu que les coroutines elles-mêmes sont toujours compilées en code natif.

Considérons le fragment de programme ci-dessous :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}

En inspectant l'IL généré pour ce fragment, nous pouvons voir cette séquence d'IL :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
IL_0000:  ldc.i4.0
IL_0001:  stloc.2
IL_0002:  ldc.i4.0
IL_0003:  stloc.0
IL_0004:  ldloca.s   V_8
IL_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

ainsi que :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
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

montrant que answer() est une méthode native et que l'appel à cette méthode dans le fragment de désassemblage MSIL ci-dessus est un appel interop. Ceci n'est montré que pour l'exposition et l'utilisateur n'a absolument rien à faire pour que cela fonctionne.

Concepts

Les concepts étant un mécanisme permettant d'effectuer des calculs sur les types au moment de la compilation, il n'y a pas de composant d'exécution à prendre en charge par .NET. En outre, les concepts "disparaissent" une fois que les modèles sont spécialisés et que les modèles sont pris en charge par C++/CLI depuis le début. Il existe deux types de modèles pris en charge par C++/CLI : les modèles ISO C++ et les modèles gérés dont la spécialisation donne lieu à des types gérés. Nous avons choisi de séparer tous les types gérés des concepts, y compris les modèles gérés. Toute tentative de mélanger des types gérés et des concepts entraîne un échec de la compilation avec des diagnostics. Notez que cela exclut les types comme System::Int32 qui peuvent être mappés directement vers des types natifs, mais la mise en boîte de ces types est également exclue de l'interaction avec les concepts.

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#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
}

Dans l'exemple ci-dessus, les types natifs fonctionnent exactement comme pour la compilation ISO C++ sans /clr, mais la tentative d'utilisation des concepts avec les types C++/CLI génère un diagnostic. Il est possible d'élargir les concepts pour permettre un sous-ensemble soigneusement choisi de l'univers des types C++/CLI, mais pour cette version de MSVC, nous avons choisi de les garder séparés. En supprimant la ligne contenant le diagnostic et en inspectant le désassemblage, nous constatons que

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
IL_0009:  ldloca.s   V_3
IL_000b:  ldloca.s   V_2
IL_000d:  call       void
modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl)
  'Swap<struct SwapMe>'(
    valuetype SwapMe* modopt([mscorlib]System.Runtime.CompilerServices.IsImplicitlyDereferenced),
    valuetype SwapMe* modopt([mscorlib]System.Runtime.CompilerServices.IsImplicitlyDereferenced))

Notez que les types de paramètres de la fonction sont SwapMe* et que le concept std::swappable n'apparaît pas.

Modules

Comme mentionné plus haut, en C++/CLI, les modules ne peuvent être importés que sous forme de modules ISO C++ normaux ou d'unités d'en-tête, créées à partir d'en-têtes qui ne contiennent pas de code C++/CLI. C'est pourquoi nous avons ces restrictions sur les modules :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
module m;               // error under /clr
export module m;        // error under /clr
export class X;
export import m;        // error under /clr
import m;               // OK with /clr
import <vector>;        // header units OK with /clr

En ligne de commande, certaines combinaisons de flags produiront des erreurs :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
cl /exportHeader /clr ...       # error
cl /ifcOutput /clr ...          # error
cl /ifcOnly /clr ...            # error

Interopérabilité et importation de modules

Puisque, actuellement, nous n'autorisons pas l'exportation de modules sous /clr, et puisque les modules peuvent avoir du code dans les fichiers .obj associés, à moins d'être compilés avec /ifcOnly, il s'ensuit qu'un appel à toute fonction non linéaire importée doit être un appel interop au code natif. Pour les modèles, cette restriction n'est pas nécessaire et l'importation, par exemple, du modèle de classe std::vector, peut entraîner la compilation de ses fonctions membres en code géré. Il s'agit d'une considération importante pour les performances, puisque les appels interop empêcheront l'inlining.

Conclusion et invitation

Le support de C++20 est ajouté à C++/CLI avec très peu de restrictions en ce qui concerne les types natifs. Le principe général suivi est celui de la moindre surprise : si une caractéristique particulière est valide dans une compilation native, il y a de fortes chances qu'elle le soit aussi sous /clr.

Si vous avez du code C++/CLI, nous vous encourageons à activer la compilation C++20, à essayer le produit et à signaler les bogues via Visual Studio Feedback. Si vous avez un besoin spécifique de modules à utiliser en conjonction avec C++/CLI, nous vous remercions à nouveau de nous faire part de vos commentaires.
Source : Microsoft

Et vous ?

Que pensez-vous de cette prise en charge de C++20 pour C++/CLI ? Trouvez-vous que ce support sera bénéfique pour vos projets ?

Voir aussi

C++ 20 : la spécification de la nouvelle version du langage C++ est finalisée, un tour d'horizon des nouveautés apportées

La spécification du C++ 20 a été approuvée à l'unanimité, elle succèdera à C++ 17 et apporte bon nombre de nouveautés dont les Modules, les Coroutines et autres

C++ 20 est publié avec de nouvelles fonctionnalités pour le langage et la bibliothèque, de nouveaux mots clés et rend obsolètes certaines anciennes fonctionnalités