27 Mart 2024

Kodu C#'tan C++'a Çevirme Kuralları: Nesne Oluşturma ve Yöntem Çağrıları

Bazen C# ve C++ ile yazılan kodun davranışı farklı olabilir. CodePorting.Translator Cs2Cpp'nin böyle farklılıkları nasıl ele aldığını ve kod çevirisinin doğruluğunu nasıl sağladığını daha yakından inceleyelim. Ayrıca birim testlerinin dönüşümünün nasıl gerçekleştirildiğini öğreneceğiz.

Nesne Oluşturma ve Başlatma

Referans türü nesneler oluşturmak için MakeObject işlevini kullanıyoruz (bu, std::make_shared ile benzerdir). Bu işlev, nesneyi new operatörü kullanarak oluşturur ve hemen bir SharedPtr içine sarar. Bu işlev, ham işaretçiler tarafından tanıtılan sorunlardan kaçınmamıza olanak tanımıştır, ancak erişim hakları sorunu yaratmıştır: çünkü bu işlev tüm sınıfların dışında olduğundan, özel yapıcı fonksiyonlara erişimi yoktu. Bu işlevi, özel yapıcılara sahip sınıfların arkadaşı olarak bildirmek, böylece tüm bağlamlarda böyle sınıfların oluşturulmasını mümkün kılacaktı. Sonuç olarak, bu işlevin dış sürümü, yalnızca genel yapıcılarla kullanım için sınırlıydı ve aynı erişim düzeyine ve aynı argümanlara sahip statik MakeObject yöntemleri, özel yapıcılar için eklendi.

C# programcıları, nesne oluşturma ifadesinin bir parçası olarak özellik başlatıcılarını sıkça kullanır. Karşılık gelen sözdizimi, başlatıcıları tek bir ifadenin bir parçası olarak yazmak mümkün olmadığından, lambda işlevleri içine sarılmalıdır:

Foo(new MyClass() { Property1 = "abc", Property2 = 1, Field1 = 3.14 });
Foo([&]{ auto tmp_0 = System::MakeObject<MyClass>();
        tmp_0->set_Property1(u"abc");
        tmp_0->set_Property2(1);
        tmp_0->Field1 = 3.14;
        return tmp_0;
    }());

Çağrılar, Delegeler ve Anonim Yöntemler

Yöntem çağrıları olduğu gibi aktarılır. Aşırı yüklenmiş yöntemlerle uğraşırken, bazen C++'daki aşırı yüklemeleri çözme kuralları C#'daki kurallardan farklı olduğundan, argüman türlerini açıkça dönüştürmek gerekebilir. Örneğin, aşağıdaki kodu düşünün:

class MyClass<T>
{
    public void Foo(string s) { }
    public void Bar(string s) { }
    public void Bar(bool b) { }
    public void Call()
    {
        Foo("abc");
        Bar("def");
    }
}

Çeviri sonrası, şu şekilde görünüyor:

template<typename T>
class MyClass : public System::Object
{
public:
    void Foo(System::String s)
    {
        CODEPORTING_UNUSED(s);
    }
    void Bar(System::String s)
    {
        CODEPORTING_UNUSED(s);
    }
    void Bar(bool b)
    {
        CODEPORTING_UNUSED(b);
    }
    void Call()
    {
        Foo(u"abc");
        Bar(System::String(u"def"));
    }
};

Not: Call yöntemi içindeki Foo ve Bar yöntem çağrıları farklı yazılmıştır. Bu, System::String yapıcısına açıkça çağrı yapılmadığında, bool kabul eden Bar aşırı yüklemesinin çağrılacağı anlamına gelir, çünkü bu tür dönüşümü C++ kurallarına göre daha yüksek önceliklidir. Foo yöntemi durumunda böyle bir belirsizlik olmadığından, çevirmen daha basit kod üretir.

C# ve C++ arasındaki farklılığın başka bir örneği şablon genişlemesidir. C#'da tür parametre değiştirme çalışma zamanında gerçekleşir ve genel yöntemler içindeki çağrıların çözümünü etkilemez. C++'da şablon argümanı değiştirme derleme zamanında gerçekleşir, bu nedenle C# davranışı taklit edilmelidir. Örneğin, aşağıdaki kodu düşünün:

class GenericMethods
{
    public void Foo<T>(T value) { }
    public void Foo(string s) { }
    public void Bar<T>(T value)
    {
        Foo(value);
    }
    public void Call()
    {
        Bar("abc");
    }
}
class GenericMethods : public System::Object
{
public:
    template <typename T>
    void Foo(T value)
    {
        CODEPORTING_UNUSED(value);
    }
    void Foo(System::String s);
    template <typename T>
    void Bar(T value)
    {
        Foo<T>(value);
    }
    void Call();
};
void GenericMethods::Foo(System::String s)
{
}
void GenericMethods::Call()
{
    Bar<System::String>(u"abc");
}

Burada, Foo ve Bar'ı çağırırken şablon argümanlarının açıkça belirtilmesi önemlidir. İlk durumda, aksi takdirde T=System::String için sürüm oluşturulduğunda, şablonsuz sürüm çağrılır ve bu C# davranışından farklıdır. İkinci durumda, argüman gereklidir, aksi takdirde dize harf türüne dayalı olarak çıkarılır. Genel olarak, çevirmenin beklenmedik davranışları önlemek için şablon argümanlarını açıkça belirtmesi neredeyse her zaman gereklidir.

Birçok durumda, çevirmenin C# ile olmayan yerlerde açıkça çağrılar oluşturması gerekmektedir – bu özellikle özelliklere ve dizinlere erişim yöntemlerini içerir. Referans türlerinin yapıcılarına yapılan çağrılar, yukarıda gösterildiği gibi MakeObject içinde sarmalanır.

.NET'te, params sözdizimi aracılığıyla argüman sayısı ve türüne göre aşırı yüklemeyi destekleyen yöntemler bulunmaktadır. Örneğin, StringBuilder.Append() ve Console.WriteLine() için böyle aşırı yüklemeler mevcuttur. Bu tür yapıların doğrudan aktarılması, kutulama ve geçici dizilerin oluşturulması nedeniyle düşük performans gösterir. Bu tür durumlarda, değişken sayıda rastgele türdeki argümanları kabul eden bir aşırı yükleme ekleriz ve argümanları tür dönüşümleri ve dizilere birleştirme olmadan olduğu gibi çeviririz. Sonuç olarak, bu tür çağrıların performansı artar.

Delegeler, genellikle içinde bir std::function örneği bulunan MulticastDelegate şablonunun özelleştirmelerine çevrilir. Onların çağrısı, depolanması ve ataması basitçe gerçekleştirilir. Anonim yöntemler lambda işlevlerine dönüştürülür.

Lambda işlevleri oluştururken, yakalanan değişkenlerin ve argümanların ömrünü uzatmak gereklidir, bu da kodu karmaşıklaştırır, bu nedenle çevirmen bunu yalnızca lambda işlevinin çevresel bağlamı aşma şansına sahip olduğu yerlerde yapar. Bu davranış (değişkenlerin ömrünü uzatma, referans veya değerle yakalama) daha optimal kod elde etmek için manuel olarak da kontrol edilebilir.

İstisnalar

İstisna işlemleri açısından C# davranışını taklit etmek oldukça karmaşıktır. Çünkü C# ve C++ istisnaları farklı şekilde davranır:

  • C#'da istisnalar yığında oluşturulur ve çöp toplayıcı tarafından silinir.
  • C++'da istisnalar farklı zamanlarda yığın ve ayrılmış bir bellek alanı arasında kopyalanır.

Bu, bir çelişki oluşturur. Eğer C# istisna türleri referans türleri olarak çevrilirse, onlarla ham işaretçiler aracılığıyla çalışmak (throw new ArgumentException), bellek sızıntılarına veya silme noktalarını belirleme konusunda önemli sorunlara yol açar. Eğer referans türleri olarak çevrilirlerse ama akıllı bir işaretçi aracılığıyla sahiplenilirlerse (throw SharedPtr<ArgumentException>(MakeObject<ArgumentException>())), istisna, temel türü tarafından yakalanamaz, çünkü SharedPtr<ArgumentException>, SharedPtr<Exception>'dan miras almaz. Ancak istisna nesneleri yığına yerleştirilirse, bunlar doğru bir şekilde temel tür tarafından yakalanır, ancak temel türün bir değişkenine kaydedildiğinde, nihai tür hakkındaki bilgiler kırpılır.

Bu sorunu çözmek için özel bir akıllı işaretçi türü olan ExceptionWrapper oluşturduk. Temel özelliği, eğer ArgumentException sınıfı Exception'dan miras alıyorsa, ExceptionWrapper<ArgumentException>'ın da ExceptionWrapper<Exception>'dan miras almasıdır. ExceptionWrapper örnekleri, istisna sınıf örneklerinin ömrünü yönetmek için kullanılır ve ExceptionWrapper türünün kırpılması, ilişkili Exception türünün kırpılmasına yol açmaz. İstisnaların fırlatılması, Exception soyundan gelen sınıflar tarafından geçersiz kılınan sanal bir yöntem tarafından ele alınır. Bu yöntem, nihai istisna türüne göre parametreli bir ExceptionWrapper oluşturur ve onu fırlatır. Sanal doğa, ExceptionWrapper türü daha önce kırpılmış olsa bile doğru türde istisna fırlatılmasına izin verir ve istisna nesnesi ile ExceptionWrapper arasındaki bağlantı bellek sızıntılarını önler.

Testler

Çerçevemizin güçlü yönlerinden biri, sadece kaynak kodunu değil, aynı zamanda birim testlerini de çevirebilme yeteneğidir.

C# programcıları, NUnit ve xUnit çerçevelerini kullanır. Çevirmen, ilgili test örneklerini GoogleTest'e çevirir ve denetimlerin sözdizimini ve Test veya Fact bayrağı ile işaretlenmiş yöntemleri ilgili test işlevlerinden çağırma şeklini değiştirir. Argümansız testler ve TestCase veya TestCaseData gibi giriş verileri de desteklenir. Aşağıda çevrilmiş bir test sınıfının örneği verilmiştir.

[TestFixture]
class MyTestCase
{
    [Test]
    public void Test1()
    {
        Assert.AreEqual(2*2, 4);
    }
    [TestCase("123")]
    [TestCase("abc")]
    public void Test2(string s)
    {
        Assert.NotNull(s);
    }
}
class MyTestCase : public System::Object
{
public:
    void Test1();
    void Test2(System::String s);
};

namespace gtest_test
{

class MyTestCase : public ::testing::Test
{
protected:
    static System::SharedPtr<::ClassLibrary1::MyTestCase> s_instance;
    
public:
    static void SetUpTestCase()
    {
        s_instance = System::MakeObject<::ClassLibrary1::MyTestCase>();
    };
    
    static void TearDownTestCase()
    {
        s_instance = nullptr;
    };
};

System::SharedPtr<::ClassLibrary1::MyTestCase> MyTestCase::s_instance;

} // namespace gtest_test

void MyTestCase::Test1()
{
    ASSERT_EQ(2 * 2, 4);
}

namespace gtest_test
{

TEST_F(MyTestCase, Test1)
{
    s_instance->Test1();
}

} // namespace gtest_test

void MyTestCase::Test2(System::String s)
{
    ASSERT_FALSE(System::TestTools::IsNull(s));
}

namespace gtest_test
{

using MyTestCase_Test2_Args = System::MethodArgumentTuple<decltype(
    &ClassLibrary1::MyTestCase::Test2)>::type;

struct MyTestCase_Test2 : public MyTestCase, public ClassLibrary1::MyTestCase,
    public ::testing::WithParamInterface<MyTestCase_Test2_Args>
{
    static std::vector<ParamType> TestCases()
    {
        return
        {
            std::make_tuple(u"123"),
            std::make_tuple(u"abc"),
        };
    }
};

TEST_P(MyTestCase_Test2, Test)
{
    const auto& params = GetParam();
    ASSERT_NO_FATAL_FAILURE(s_instance->Test2(std::get<0>(params)));
}

INSTANTIATE_TEST_SUITE_P(, MyTestCase_Test2, 
    ::testing::ValuesIn(MyTestCase_Test2::TestCases()));

} // namespace gtest_test

İlgili Haberler

İlgili videolar

İlgili makaleler