27 Mart 2024
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.
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;
}());
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.
İ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:
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.
Ç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