22 mars 2024

Règles de traduction du code de C# à C++ : Membres de classe et structures de contrôle

Dans cet article, nous explorerons comment notre traducteur convertit les membres de classe, les variables, les champs, les opérateurs et les structures de contrôle C#. Nous aborderons également l'utilisation de la bibliothèque de support du traducteur pour la conversion correcte des types du .NET Framework en C++.

Membres de la classe

Les méthodes de classe se mappent directement sur C++. Cela s'applique également aux méthodes statiques et aux constructeurs. Dans certains cas, du code supplémentaire peut apparaître – par exemple, pour émuler des appels à des constructeurs statiques. Les méthodes d'extension et les opérateurs sont traduits en méthodes statiques et sont appelés explicitement. Les finaliseurs deviennent des destructeurs.

Les champs d'instance C# deviennent des champs d'instance C++. Les champs statiques restent également inchangés, sauf dans les cas où l'ordre d'initialisation est important – cela est implémenté en traduisant de tels champs comme des singletons.

Les propriétés sont divisées en une méthode getter et une méthode setter, ou juste une si la seconde méthode est absente. Pour les auto-propriétés, un champ de valeur privé est également ajouté. Les propriétés statiques sont divisées en un getter statique et un setter. Les indexeurs sont traités en utilisant la même logique.

Les événements sont traduits en champs, dont le type correspond à la spécialisation requise de System::Event. La traduction sous forme de trois méthodes (add, remove et invoke) serait plus correcte et, de plus, permettrait de supporter des événements abstraits et virtuels. Il est possible que, à l'avenir, nous adoptions un tel modèle, mais pour le moment l'option de classe Event couvre entièrement nos besoins.

L'exemple suivant illustre les règles ci-dessus :

public abstract class Generic<T>
{
    private T m_value;
    public Generic(T value)
    {
        m_value = value;
    }
    ~Generic()
    {
        m_value = default(T);
    }
    public string Property { get; set; }
    public abstract int Property2 { get; }
    public T this[int index]
    {
        get
        {
            return index == 0 ? m_value : default(T);
        }
        set
        {
            if (index == 0)
                m_value = value;
            else
                throw new ArgumentException();
        }
    }
    public event Action<int, int> IntIntEvent;
}

Résultat de la traduction C++ (code insignifiant supprimé) :

template<typename T>
class Generic : public System::Object
{
public:
    System::String get_Property()
    {
        return pr_Property;
    }
    void set_Property(System::String value)
    {
        pr_Property = value;
    }
    
    virtual int32_t get_Property2() = 0;
    
    Generic(T value) : m_value(T())
    {
        m_value = value;
    }
    
    T idx_get(int32_t index)
    {
        return index == 0 ? m_value : System::Default<T>();
    }
    void idx_set(int32_t index, T value)
    {
        if (index == 0)
        {
            m_value = value;
        }
        else
        {
            throw System::ArgumentException();
        }
    }
    
    System::Event<void(int32_t, int32_t)> IntIntEvent;
    
    virtual ~Generic()
    {
        m_value = System::Default<T>();
    }

private:
    T m_value;
    System::String pr_Property;
};

Variables et champs

Les champs constants et statiques sont traduits en champs statiques, constantes statiques (dans certains cas – constexpr), ou en méthodes statiques qui fournissent un accès à un singleton. Les champs d'instance C# sont convertis en champs d'instance C++. Tout initialiseur complexe est déplacé vers les constructeurs, et parfois il est nécessaire d'ajouter explicitement des constructeurs par défaut là où ils n'existaient pas en C#. Les variables de pile sont passées telles quelles. Les arguments de méthode sont également passés tels quels, sauf que les arguments ref et out deviennent des références (heureusement, la surcharge sur ceux-ci est interdite).

Les types de champs et de variables sont remplacés par leurs équivalents C++. Dans la plupart des cas, de tels équivalents sont générés par le traducteur lui-même à partir du code source C#. Les types de bibliothèque, y compris les types de .NET Framework et d'autres, sont écrits par nous en C++ et font partie de la bibliothèque de support du traducteur, qui est fournie avec les produits convertis. var est traduit en auto, sauf dans les cas où une indication de type explicite est nécessaire pour lisser les différences de comportement.

De plus, les types de référence sont enveloppés dans SmartPtr. Les types de valeur sont substitués tels quels. Puisque les arguments de type peuvent être soit des types de valeur soit des types de référence, ils sont également substitués tels quels, mais lorsqu'ils sont instanciés, les arguments de référence sont enveloppés dans SharedPtr. Ainsi, List<int> est traduit par List<int32_t>, mais List<Object> devient List<SmartPtr<Object>>. Dans certains cas exceptionnels, les types de référence sont traduits en types de valeur. Par exemple, notre mise en œuvre de System::String est basée sur le type UnicodeString de ICU et optimisée pour le stockage en pile.

Pour illustrer, traduisons la classe suivante :

public class Variables
{
    public int m_int;
    private string m_string = new StringBuilder().Append("foobazz").ToString();
    private Regex m_regex = new Regex("foo|bar");
    public object Foo(int a, out int b)
    {
        b = a + m_int;
        return m_regex.Match(m_string);
    }
}

Après traduction, il prend la forme suivante (code insignifiant supprimé) :

class Variables : public System::Object
{
public:
    int32_t m_int;
    System::SharedPtr<System::Object> Foo(int32_t a, int32_t& b);
    Variables();
private:
    System::String m_string;
    System::SharedPtr<System::Text::RegularExpressions::Regex> m_regex;
};
System::SharedPtr<System::Object> Variables::Foo(int32_t a, int32_t& b)
{
    b = a + m_int;
    return m_regex->Match(m_string);
}
Variables::Variables()
    : m_int(0)
    , m_regex(System::MakeObject<System::Text::RegularExpressions::Regex>(u"foo|bar"))
{
    this->m_string = System::MakeObject<System::Text::StringBuilder>()->
        Append(u"foobazz")->ToString();
}

Structures de contrôle

La similitude des principales structures de contrôle a joué en notre faveur. Des opérateurs tels que if, else, switch, while, do-while, for, try-catch, return, break et continue sont pour la plupart transférés tels quels. L'exception dans cette liste est peut-être seulement le switch, qui nécessite quelques traitements spéciaux. Premièrement, C# permet son utilisation avec le type chaîne de caractères—dans C++, nous générons une séquence de if-else if dans ce cas. Deuxièmement, l'ajout relativement récent de la capacité à faire correspondre l'expression vérifiée à un modèle de type—ce qui, cependant, est également facilement déplié en une séquence de if.

Les constructions qui ne sont pas présentes en C++ sont intéressantes. Ainsi, l'opérateur using garantit l'appel de la méthode Dispose() en sortant du contexte. En C++, nous émulons ce comportement en créant un objet de garde sur la pile, qui appelle la méthode requise dans son destructeur. Avant cela, cependant, il est nécessaire de capturer l'exception lancée par le code qui était le corps de using, et de stocker le exception_ptr dans le champ de la garde—si Dispose() ne lance pas son exception, celle que nous avons stockée sera relancée. C'est justement ce cas rare où le lancement d'une exception depuis un destructeur est justifié et n'est pas une erreur. Le bloc finally est traduit selon un schéma similaire, seulement au lieu de la méthode Dispose(), une fonction lambda est appelée, dans laquelle le traducteur a enveloppé son corps.

Un autre opérateur qui n'est pas présent en C# et que nous sommes forcés d'émuler est foreach. Initialement, nous l'avons traduit en un while équivalent, appelant la méthode MoveNext() de l'énumérateur, qui est universelle mais assez lente. Comme la plupart des implémentations C++ des conteneurs .NET utilisent des structures de données STL, nous avons commencé à utiliser leurs itérateurs originaux lorsque c'est possible, convertissant foreach en for basé sur une plage. Dans les cas où les itérateurs originaux ne sont pas disponibles (par exemple, le conteneur est implémenté en C# pur), des itérateurs enveloppeurs sont utilisés, qui travaillent avec des énumérateurs en interne. Auparavant, le choix de la bonne méthode d'itération était la responsabilité d'une fonction externe, écrite en utilisant la technique SFINAE, maintenant nous sommes proches d'avoir les bonnes versions des méthodes begin-end dans tous les conteneurs, y compris ceux traduits.

Opérateurs

Comme pour les structures de contrôle, la plupart des opérateurs (au moins arithmétiques, logiques et d'assignation) ne nécessitent pas de traitement spécial. Cependant, il y a un point subtil : en C#, l'ordre d'évaluation des parties d'une expression est déterministe, tandis qu'en C++ il peut y avoir un comportement indéfini dans certains cas. Par exemple, le code traduit suivant se comporte différemment après compilation par différents outils :

auto offset32 = block[i++] + block[i++] * 256 + block[i++] * 256 * 256 +
    block[i++] * 256 * 256 * 256;

Heureusement, de tels problèmes sont assez rares. Nous avons l'intention d'enseigner au traducteur comment gérer de tels moments, mais en raison de la complexité de l'analyse qui identifie les expressions avec des effets secondaires, cela n'a pas encore été mis en œuvre.

Cependant, même les opérateurs les plus simples nécessitent un traitement spécial lorsqu'ils sont appliqués aux propriétés. Comme indiqué ci-dessus, les propriétés sont divisées en accesseurs et mutateurs, et le traducteur doit insérer les appels nécessaires en fonction du contexte :

obj1.Property = obj2.Property;
string s = GetObj().Property += "suffix";
obj1->set_Property(obj2->get_Property());
System::String s = System::setter_add_wrap(static_cast<MyClass*>(GetObj().GetPointer()),
    &MyClass::get_Property, &MyClass::set_Property, u"suffix")

Dans la première ligne, le remplacement s'est avéré être trivial. Dans la seconde, il a été nécessaire d'utiliser l'enveloppe setter_add_wrap, assurant que la fonction GetObj() soit appelée une seule fois, et que le résultat de la concaténation de l'appel à get_Property() et du littéral de chaîne soit transmis non seulement à la méthode set_Property() (qui retourne void), mais aussi plus loin pour être utilisé dans l'expression. La même approche est appliquée lors de l'accès aux indexeurs.

Les opérateurs C# qui ne sont pas en C++ : as, is, typeof, default, ??, ?., et ainsi de suite, sont émulés en utilisant des fonctions de la bibliothèque de support du traducteur. Dans les cas où il est nécessaire d'éviter une double évaluation des arguments, par exemple, pour ne pas déplier GetObj()?.Invoke() en GetObj() ? GetObj().Invoke() : nullptr, une approche similaire à celle montrée ci-dessus est utilisée.

L'opérateur d'accès aux membres (.) peut être remplacé par un équivalent de C++ en fonction du contexte : l'opérateur de résolution de portée (::) ou la “flèche” (->). Un tel remplacement n'est pas requis lors de l'accès aux membres des structures.

Nouvelles connexes

Vidéos associées

Articles liés