Kodu C#'tan C++'a Çevirme Kuralları: Sınıf Üyeleri ve Kontrol Yapıları

Bu makalede, çevirmenimizin sınıf üyelerini, değişkenleri, alanları, operatörleri ve C# kontrol yapılarını nasıl dönüştürdüğünü inceleyeceğiz. Ayrıca, .NET Framework türlerini doğru bir şekilde C++'a dönüştürmek için çevirmen destek kütüphanesinin kullanımına da değineceğiz.

Sınıf Üyeleri

Sınıf yöntemleri doğrudan C++'a eşlenir. Bu, aynı zamanda statik yöntemler ve yapıcılar için de geçerlidir. Bazı durumlarda, örneğin statik yapıcıları çağırmak için ek kod görünebilir. Uzantı yöntemleri ve operatörler, statik yöntemlere dönüştürülür ve açıkça çağrılır. Finalize yöntemleri yıkıcılar haline gelir.

C# örnek alanları C++ örnek alanlarına dönüşür. Statik alanlar da değişmez, ancak başlatma sırasının önemli olduğu durumlarda (bu, böyle alanların tek örnek olarak çevrildiği şekilde uygulanır).

Özellikler, bir alıcı yöntemi ve bir ayarlayıcı yöntemi olarak bölünür veya ikinci yöntem yoksa sadece bir yöntem olarak kalır. Otomatik özellikler için özel bir değer alanı da eklenir. Statik özellikler, bir statik alıcı ve ayarlayıcı olarak bölünür. İndeksleyiciler aynı mantıkla işlenir.

Etkinlikler, türü System::Event'in gerekli özelleştirmesine karşılık gelen alanlara çevrilir. Üç yöntemli bir çeviri (add, remove ve invoke) daha doğru olurdu ve ayrıca soyut ve sanal etkinlikleri desteklememizi sağlardı. Muhtemelen gelecekte böyle bir modele ulaşacağız, ancak şu anda Event sınıf seçeneği ihtiyaçlarımızı tamamen karşılamaktadır.

Aşağıdaki örnek, yukarıdaki kuralları göstermektedir:

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;
}

C++ çeviri sonucu (önemsiz kodlar çıkarıldı):

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;
};

Değişkenler ve alanlar

Sabit ve statik alanlar, statik alanlar, statik sabitler (bazı durumlarda constexpr) veya bir singleton'a erişim sağlayan statik yöntemlere çevrilir. C# örnek alanları C++ örnek alanlarına dönüştürülür. Herhangi bir karmaşık başlatıcı, yapıcıya taşınır ve bazen C# içinde mevcut olmayan varsayılan yapıcıları açıkça eklemek gerekebilir. Yığın değişkenleri olduğu gibi iletilir. Yöntem argümanları da olduğu gibi iletilir, ancak hem ref hem de out argümanları referanslara dönüşür (neyse ki bunlar üzerinde aşırı yükleme yasaktır).

Alanların ve değişkenlerin türleri C++ eşdeğerleriyle değiştirilir. Çoğu durumda, bu tür eşdeğerler, çevirmen tarafından C# kaynak kodundan kendiliğinden oluşturulur. .NET Framework türleri ve bazı diğerleri dahil olmak üzere kütüphane türleri, C++'da bizim tarafımızdan yazılır ve çevrilmiş ürünlerle birlikte sağlanan çevirmen destek kütüphanesinin bir parçasıdır. var, davranış farklılıklarını düzeltmek için açık tür belirtimi gerektiği durumlar dışında auto'ya çevrilir.

Ayrıca, referans türleri SmartPtr ile sarmalanır. Değer türleri olduğu gibi yerine konur. Tür argümanları değer veya referans türleri olabilir, bu nedenle bunlar da olduğu gibi yerine konur, ancak örneklendirildiğinde referans argümanları SharedPtr ile sarmalanır. Böylece, List<int> List<int32_t> olarak çevrilir, ancak List<Object> List<SmartPtr<Object>> haline gelir. Bazı istisnai durumlarda, referans türleri değer türleri olarak çevrilir. Örneğin, System::String uygulamamız, ICU kaynaklı UnicodeString türüne dayanır ve yığın depolama için optimize edilmiştir.

Örneklemek için aşağıdaki sınıfı çevirelim:

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);
    }
}

Çeviri sonrası, aşağıdaki şekli alır (önemsiz kodlar çıkarıldı):

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();
}

Kontrol yapıları

Ana kontrol yapılarının benzerliği işimize yarıyor. if, else, switch, while, do-while, for, try-catch, return, break ve continue gibi operatörler çoğunlukla olduğu gibi aktarılır. Bu listedeki tek istisna muhtemelen switch operatörüdür ve özel birkaç işlem gerektirir. İlk olarak, C# onun dize türüyle kullanılmasına izin verir—C++'da bu durumda bir if-else if dizisi oluştururuz. İkincisi, kontrol edilen ifadenin bir tür şablonuna eşleştirilme yeteneğinin nispeten yeni bir eklemesi vardır—ancak bu da kolayca bir dizi if'e açılabilir.

C++'da bulunmayan yapılar da ilgi çekicidir. Örneğin, using operatörü, bağlamdan çıkarken Dispose() yönteminin çağrısını garanti eder. C++'da bu davranışı, yığın üzerinde bir koruma nesnesi oluşturarak taklit ediyoruz ve bu nesnenin yıkıcısında gerekli yöntemi çağırıyoruz. Ancak bundan önce, using'in gövdesi olan kod tarafından atılan istisnayı yakalamak ve koruma nesnesinin alanına exception_ptr'yi saklamak gereklidir—eğer Dispose() kendi istisnasını atmazsa, sakladığımız istisna yeniden atılacaktır. Bu, yıkıcıdan istisna atmanın haklı çıkarıldığı ve hata olmadığı nadir bir durumdur. finally bloğu benzer bir düzene göre çevrilir, ancak Dispose() yöntemi yerine, çevirmenin gövdesini sardığı bir lambda işlevi çağrılır.

C# dilinde bulunmayan ve zorunda kaldığımız bir diğer operatör foreach'tir. Başlangıçta, bunu eşdeğer bir while döngüsüne çevirdik ve sayacın MoveNext() yöntemini çağırdık. Bu yöntem evrensel olmakla birlikte oldukça yavaştır. .NET konteynerlerinin çoğu C++ uygulaması STL veri yapılarını kullandığından, mümkün olduğunda orijinal iteratörlerini kullanmaya başladık ve foreach'ü aralık tabanlı for döngüsüne dönüştürdük. Orijinal iteratörlerin kullanılamadığı durumlarda (örneğin, konteyner saf C# ile uygulanmışsa), içsel olarak numaralandırıcılarla çalışan sarmal iteratörler kullanılır. Daha önce doğru iterasyon yönteminin seçimi, SFINAE tekniği kullanılarak yazılmış harici bir işlevin sorumluluğuydu, şimdi ise tüm konteynerlerde, çevrilenler dahil, begin-end yöntemlerinin doğru sürümlerine sahip olmaya yakınız.

Operatörler

Kontrol yapılarıyla aynı şekilde, çoğu operatör (en azından aritmetik, mantıksal ve atama operatörleri) özel işlem gerektirmez. Ancak, ince bir nokta vardır: C# dilinde bir ifadenin parçalarının değerlendirme sırası belirlidir, oysa C++ dilinde bazı durumlarda tanımsız davranış olabilir. Örneğin, aşağıdaki çevrilmiş kod, farklı araçlar tarafından derlendikten sonra farklı davranır:

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

Neyse ki, böyle sorunlar oldukça nadirdir. Bu tür anları ele almak için çevirmeni eğitmek için planlarımız var, ancak yan etkileri olan ifadeleri tanımlayan analizin karmaşıklığı nedeniyle henüz uygulanmadı.

Ancak, en basit operatörler bile özellikle özelliklere uygulandığında özel işlem gerektirir. Yukarıda gösterildiği gibi, özellikler alıcılar ve ayarlayıcılar olarak ayrılır ve çevirmen, bağlama bağlı olarak gerekli çağrıları eklemek zorundadır:

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")

İlk satırda, yerine koyma işlemi basit çıktı. İkincisinde, setter_add_wrap sarmalını kullanmak gerekiyordu, böylece GetObj() işlevinin yalnızca bir kez çağrıldığından emin olunur ve get_Property() çağrısının ve dize harfle birleştirilmiş sonucunun yalnızca set_Property() yöntemine (ki bu void döndürür) değil, aynı zamanda ifadede daha fazla kullanım için geçirilir. Aynı yaklaşım, indeksleyicilere erişirken de uygulanır.

C++ dilinde olmayan C# operatörleri (as, is, typeof, default, ??, ?., vb.) çevirmen destek kütüphanesi işlevlerini kullanarak taklit edilir. Argümanların çift değerlendirmesinden kaçınılması gereken durumlarda, örneğin GetObj()?.Invoke()'u GetObj() ? GetObj().Invoke() : nullptr şeklinde açmamak için yukarıda gösterilen yaklaşım benzer şekilde kullanılır.

Üye erişim operatörü (.), bağlama bağlı olarak C++'dan bir eşdeğeri ile değiştirilebilir: kapsam çözümleme operatörü (::) veya “ok” (->). Bu tür bir değiştirme, yapı üyelerine erişirken gereksiz değildir.

İlgili makaleler