27 มีนาคม 2567
บางครั้งพฤติกรรมของโค้ดที่เขียนใน C# และ C++ อาจแตกต่างกัน มาดูกันว่า CodePorting.Translator Cs2Cpp จัดการกับความแตกต่างเหล่านี้และรับประกันความถูกต้องของการแปลโค้ดอย่างไร เราจะเรียนรู้ด้วยว่าการแปลงทดสอบหน่วยนั้นดำเนินการอย่างไร
สำหรับการสร้างออบเจ็กต์ประเภทอ้างอิง เราใช้ฟังก์ชัน MakeObject
(คล้ายกับ std::make_shared
) ซึ่งสร้างออบเจ็กต์โดยใช้ตัวดำเนินการ ใหม่
และล้อมไว้ใน SharedPtr
ทันที การใช้ฟังก์ชันนี้ช่วยให้เราหลีกเลี่ยงปัญหาที่เกิดจากพอยน์เตอร์แบบดิบได้ แต่ฟังก์ชันนี้ได้สร้างปัญหาเกี่ยวกับสิทธิ์การเข้าถึง เนื่องจากฟังก์ชันนี้อยู่นอกคลาสทั้งหมด จึงไม่สามารถเข้าถึงตัวสร้างส่วนตัวได้ การประกาศฟังก์ชันนี้ในฐานะเพื่อนของคลาสที่มีตัวสร้างที่ไม่ใช่แบบสาธารณะจะทำให้สามารถสร้างคลาสดังกล่าวได้ในทุกบริบท ด้วยเหตุนี้ เวอร์ชันภายนอกของฟังก์ชันนี้จึงถูกจำกัดให้ใช้กับตัวสร้างสาธารณะ และมีการเพิ่มเมธอด MakeObject
แบบคงที่สำหรับตัวสร้างที่ไม่ใช่สาธารณะ โดยมีระดับการเข้าถึงเดียวกันและอาร์กิวเมนต์เดียวกันกับตัวสร้างพร็อกซี
โปรแกรมเมอร์ C# มักใช้คุณสมบัติเริ่มต้นเป็นส่วนหนึ่งของนิพจน์การสร้างวัตถุ ไวยากรณ์ที่เกี่ยวข้องจะต้องถูกรวมไว้ในฟังก์ชัน lambda เพราะไม่เช่นนั้น จะไม่สามารถเขียนตัวเตรียมใช้งานให้เป็นส่วนหนึ่งของนิพจน์เดียวได้:
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;
}());
การเรียกใช้งานเมธอดจะถูกส่งต่อไปตามที่เป็นอยู่ ในการจัดการกับเมธอดที่มีการโอเวอร์โหลด บางครั้งอาจจำเป็นต้องแสดงชนิดของอาร์กิวเมนต์อย่างชัดเจน เนื่องจากกฎในการแก้ไขการโอเวอร์โหลดใน C++ นั้นแตกต่างจากใน C# ตัวอย่างเช่น พิจารณาโค้ดต่อไปนี้:
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");
}
}
หลังจากการแปล มันจะดูเป็นแบบนี้:
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"));
}
};
หมายเหตุ: การเรียกใช้งานเมธอด Foo
และ Bar
ภายในเมธอด Call
ถูกเขียนอย่างแตกต่างกัน นั่นเป็นเพราะหากไม่มีการเรียกใช้งาน System::String
อย่างชัดเจน การโอเวอร์โหลด Bar
ที่ยอมรับ bool
จะถูกเรียกใช้งาน เนื่องจากการแปลงชนิดนั้นมีความสำคัญสูงตามกฎของ C++ ในกรณีของเมธอด Foo
ไม่มีความไม่ชัดเจนดังกล่าว และตัวแปลจะสร้างโค้ดที่ง่ายกว่า
ตัวอย่างอื่นของ C# และ C++ ที่มีพฤติกรรมแตกต่างกันคือการขยายแม่แบบ ใน C# การแทนที่พารามิเตอร์ประเภทจะเกิดขึ้นในระหว่างรันไทม์และไม่ส่งผลต่อการแก้ไขการเรียกใช้งานภายในเมธอดทั่วไป ใน C++ การแทนที่อาร์กิวเมนต์แม่แบบเกิดขึ้นในระหว่างเวลาคอมไพล์ ดังนั้นจึงต้องเลียนแบบพฤติกรรมของ C# ตัวอย่างเช่น พิจารณาโค้ดต่อไปนี้:
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");
}
ที่นี่ สำคัญที่จะต้องทราบถึงการระบุอาร์กิวเมนต์แม่แบบอย่างชัดเจนเมื่อเรียกใช้งาน Foo
และ Bar
ในกรณีแรก สิ่งนี้จำเป็นเพราะมิฉะนั้น เมื่อสร้างอินสแตนซ์สำหรับ T=System::String
จะเรียกใช้งานเวอร์ชันที่ไม่ใช่แม่แบบ ซึ่งแตกต่างจากพฤติกรรมของ C# ในกรณีที่สอง ต้องการอาร์กิวเมนต์เพราะมิฉะนั้นจะถูกสรุปตามชนิดของตัวอักษรที่กำหนด โดยทั่วไปแล้ว ตัวแปลจะต้องระบุอาร์กิวเมนต์แม่แบบอย่างชัดเจนเพื่อหลีกเลี่ยงพฤติกรรมที่ไม่คาดคิด
ในหลายกรณี ตัวแปลต้องสร้างการเรียกใช้งานอย่างชัดเจนที่ไม่มีอยู่ใน C# – สิ่งนี้ส่วนใหญ่เกี่ยวข้องกับวิธีการเข้าถึงคุณสมบัติและดัชนี การเรียกใช้งานตัวสร้างของประเภทอ้างอิงจะถูกห่อหุ้มใน MakeObject
ตามที่แสดงไว้ข้างต้น
ใน .NET มีเมธอดที่สนับสนุนการโอเวอร์โหลดโดยจำนวนและชนิดของอาร์กิวเมนต์ผ่านไวยากรณ์ params
โดยระบุ object
เป็นชนิดของอาร์กิวเมนต์ หรือทั้งสองอย่างพร้อมกัน – ตัวอย่างเช่น มีการโอเวอร์โหลดดังกล่าวสำหรับ StringBuilder.Append()
และ Console.WriteLine()
การถ่ายโอนโครงสร้างดังกล่าวโดยตรงแสดงให้เห็นถึงประสิทธิภาพที่ต่ำเนื่องจากการบ็อกซ์และการสร้างอาร์เรย์ชั่วคราว ในกรณีเช่นนี้ เราเพิ่มการโอเวอร์โหลดที่ยอมรับจำนวนอาร์กิวเมนต์ที่แปรผันได้ของชนิดต
ผู้รับมอบสิทธิ์จะถูกแปลเป็นความเชี่ยวชาญพิเศษของเทมเพลต MulticastDelegate
ซึ่งโดยทั่วไปจะมีคอนเทนเนอร์ของอินสแตนซ์ std::function
อยู่ข้างใน การภาวนา การจัดเก็บ และการมอบหมายงานของพวกเขานั้นดำเนินการเพียงเล็กน้อย วิธีการที่ไม่เปิดเผยตัวตนจะกลายเป็นฟังก์ชันแลมบ์ดา
เมื่อสร้างฟังก์ชัน lambda จำเป็นต้องยืดอายุของตัวแปรและอาร์กิวเมนต์ที่บันทึกไว้ ซึ่งทำให้โค้ดซับซ้อน ดังนั้นนักแปลจึงทำเช่นนี้เฉพาะในกรณีที่ฟังก์ชัน lambda มีโอกาสที่จะอยู่ได้นานกว่าบริบทโดยรอบ พฤติกรรมนี้ (การยืดอายุการใช้งานของตัวแปร การจับภาพโดยการอ้างอิงหรือตามค่า) สามารถควบคุมได้ด้วยตนเองเพื่อให้ได้โค้ดที่เหมาะสมที่สุด
การเลียนแบบพฤติกรรม C# ในแง่ของการจัดการข้อยกเว้นนั้นค่อนข้างไม่สำคัญ เนื่องจากข้อยกเว้นใน C# และ C++ มีพฤติกรรมแตกต่างออกไป:
สิ่งนี้นำเสนอความขัดแย้ง หากประเภทข้อยกเว้น C# ได้รับการแปลเป็นประเภทอ้างอิง โดยทำงานร่วมกับพอยน์เตอร์ดิบ (throw new ArgumentException
) จะทำให้หน่วยความจำรั่วหรือเกิดปัญหาสำคัญในการพิจารณาจุดลบ หากแปลเป็นประเภทการอ้างอิงแต่เป็นเจ้าของผ่านตัวชี้อัจฉริยะ (throw SharedPtr<ArgumentException>(MakeObject<ArgumentException>())
) ข้อยกเว้นจะไม่สามารถตรวจจับได้จากประเภทพื้นฐานเนื่องจาก SharedPtr<ArgumentException>
ไม่ได้สืบทอด จาก SharedPtr<Exception>
อย่างไรก็ตาม หากวางวัตถุข้อยกเว้นไว้บนสแต็ก วัตถุเหล่านั้นจะถูกจับอย่างถูกต้องตามประเภทฐาน แต่เมื่อบันทึกในตัวแปรของประเภทฐาน ข้อมูลเกี่ยวกับประเภทสุดท้ายจะถูกตัดทอน
เพื่อแก้ไขปัญหานี้ เราได้สร้างตัวชี้อัจฉริยะชนิดพิเศษ ExceptionWrapper
คุณลักษณะที่สำคัญของมันคือหากคลาส ArgumentException
สืบทอดมาจาก Exception
ดังนั้น ExceptionWrapper<ArgumentException>
ก็สืบทอดมาจาก ExceptionWrapper<Exception>
ด้วยเช่นกัน อินสแตนซ์ของ ExceptionWrapper
ใช้เพื่อจัดการอายุการใช้งานของอินสแตนซ์คลาสข้อยกเว้น และการตัดทอนประเภท ExceptionWrapper
จะไม่นำไปสู่การตัดทอนประเภท Exception
ที่เกี่ยวข้อง การขว้างข้อยกเว้นได้รับการจัดการโดยวิธีการเสมือน ซึ่งถูกแทนที่โดยลูกหลาน Exception
ซึ่งสร้าง ExceptionWrapper
ที่กำหนดพารามิเตอร์ตามประเภทข้อยกเว้นสุดท้ายแล้วโยนทิ้ง ลักษณะเสมือนช่วยให้สามารถโยนประเภทข้อยกเว้นที่ถูกต้องได้ แม้ว่าประเภท ExceptionWrapper
จะถูกตัดทอนก่อนหน้านี้ และการเชื่อมโยงระหว่างวัตถุข้อยกเว้นกับ ExceptionWrapper
จะป้องกันการรั่วไหลของหน่วยความจำ
จุดแข็งประการหนึ่งของกรอบงานของเราคือความสามารถในการแปลไม่เพียงแต่ซอร์สโค้ดเท่านั้น แต่ยังรวมถึงการทดสอบหน่วยด้วย
โปรแกรมเมอร์ C# ใช้เฟรมเวิร์ก NUnit และ xUnit นักแปลจะแปลงตัวอย่างการทดสอบที่เกี่ยวข้องเป็น GoogleTest โดยแทนที่ไวยากรณ์ของการตรวจสอบและวิธีการเรียกที่มีเครื่องหมาย Test
หรือ Fact
จากฟังก์ชันการทดสอบที่เกี่ยวข้อง รองรับทั้งการทดสอบที่ไม่มีอาร์กิวเมนต์และข้อมูลอินพุต เช่น TestCase
หรือ TestCaseData
ตัวอย่างของคลาสการทดสอบการแปลมีดังต่อไปนี้
[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