27 มีนาคม 2568
ตั้งแต่เริ่มต้น งานนี้เกี่ยวข้องกับการพอร์ตหลายโปรเจกต์ซึ่งมีโค้ดรวมกันหลายล้านบรรทัด โดยพื้นฐานแล้ว ข้อกำหนดทางเทคนิคสำหรับตัวแปลภาษาสรุปได้เป็นวลีที่ว่า “ตรวจสอบให้แน่ใจว่าโค้ดทั้งหมดนี้สามารถพอร์ตและทำงานได้อย่างถูกต้องใน C++” งานของผู้ที่รับผิดชอบในการปล่อยผลิตภัณฑ์ C++ เกี่ยวข้องกับการแปลโค้ด การรันเทสต์ การเตรียมแพ็คเกจสำหรับรีลีส และอื่นๆ ปัญหาที่พบบ่อยมักจะอยู่ในหมวดหมู่ใดหมวดหมู่หนึ่งต่อไปนี้:
ลำดับความคืบหน้าในรายการนี้เป็นแบบจากบนลงล่าง — ตัวอย่างเช่น หากไม่แก้ไขปัญหาการคอมไพล์ในโค้ดที่แปลแล้ว ก็เป็นไปไม่ได้ที่จะตรวจสอบการทำงานและประสิทธิภาพของมัน ด้วยเหตุนี้ ปัญหาเรื้อรังหลายอย่างจึงเพิ่งถูกค้นพบในช่วงท้ายๆ ของการทำงานในโปรเจกต์ CodePorting.Translator Cs2Cpp
ในตอนแรก เมื่อทำการแก้ไขปัญหาหน่วยความจำรั่วไหลง่ายๆ ที่เกิดจากการพึ่งพากันแบบวงกลม (circular dependencies) ระหว่างอ็อบเจกต์ เราใช้แอตทริบิวต์ CppWeakPtr
กับฟิลด์ ส่งผลให้ฟิลด์มีไทป์เป็น WeakPtr
ตราบใดที่ WeakPtr
สามารถแปลงเป็น SharedPtr
ได้โดยการเรียกเมธอด lock()
หรือโดยปริยาย (ซึ่งสะดวกกว่าในทางไวยากรณ์) สิ่งนี้ก็ไม่ได้ก่อให้เกิดปัญหา อย่างไรก็ตาม ต่อมาเรายังต้องทำให้การอ้างอิงที่อยู่ในคอนเทนเนอร์เป็นแบบ weak ด้วย โดยใช้ไวยากรณ์พิเศษสำหรับแอตทริบิวต์ CppWeakPtr
และสิ่งนี้นำไปสู่เรื่องน่าประหลาดใจที่ไม่พึงประสงค์สองสามอย่าง
สัญญาณแรกของปัญหาเกี่ยวกับแนวทางที่เราใช้คือ จากมุมมองของ C++, MyContainer<SharedPtr<MyClass>>
และ MyContainer<WeakPtr<MyClass>>
เป็นไทป์ที่แตกต่างกันสองไทป์ ด้วยเหตุนี้จึงไม่สามารถเก็บไว้ในตัวแปรเดียวกัน ส่งผ่านไปยังเมธอดเดียวกัน ส่งคืนจากเมธอดนั้น และอื่นๆ ได้ แอตทริบิวต์ซึ่งเดิมทีมีจุดประสงค์เพื่อจัดการวิธีการจัดเก็บการอ้างอิงในฟิลด์ของอ็อบเจกต์เท่านั้น เริ่มปรากฏในบริบทที่แปลกประหลาดมากขึ้นเรื่อยๆ ส่งผลกระทบต่อค่าส่งคืน อาร์กิวเมนต์ ตัวแปรโลคัล ฯลฯ โค้ดของตัวแปลภาษาที่รับผิดชอบในการจัดการกับมันก็ซับซ้อนขึ้นทุกวัน
ปัญหาที่สองก็เป็นสิ่งที่เราไม่ได้คาดการณ์ไว้เช่นกัน สำหรับโปรแกรมเมอร์ C# กลายเป็นเรื่องปกติที่จะมีคอลเลกชันแบบจับคู่ (associative collection) เพียงหนึ่งรายการต่ออ็อบเจกต์ ซึ่งมีทั้งการอ้างอิงที่ไม่ซ้ำกัน (unique references) ไปยังอ็อบเจกต์ที่อ็อบเจกต์ปัจจุบันเป็นเจ้าของและไม่สามารถเข้าถึงได้ด้วยวิธีอื่น รวมถึงการอ้างอิงไปยังอ็อบเจกต์แม่ (parent objects) ด้วย การทำเช่นนี้เพื่อเพิ่มประสิทธิภาพการดำเนินการอ่านจากรูปแบบไฟล์บางอย่าง แต่สำหรับเรา มันหมายความว่าคอลเลกชันเดียวกันสามารถมีการอ้างอิงทั้งแบบ strong และ weak ได้ ไทป์ของพอยเตอร์จึงไม่ใช่ตัวกำหนดสุดท้ายของโหมดการทำงานของมันอีกต่อไป
เห็นได้ชัดว่า ปัญหาสองข้อนี้ไม่สามารถแก้ไขได้ภายในกระบวนทัศน์ที่มีอยู่ และไทป์ของพอยเตอร์จึงได้รับการพิจารณาใหม่ ผลลัพธ์ของแนวทางที่ปรับปรุงใหม่นี้คือคลาส SmartPtr
ซึ่งมีเมธอด set_Mode()
ที่รับค่าหนึ่งในสองค่า: SmartPtrMode::Shared
และ SmartPtrMode::Weak
คอนสตรักเตอร์ทั้งหมดของ SmartPtr
ก็รับค่าเดียวกันเหล่านี้ ด้วยเหตุนี้ อินสแตนซ์ของพอยเตอร์แต่ละตัวจึงสามารถอยู่ในสถานะใดสถานะหนึ่งจากสองสถานะ:
การสลับระหว่างโหมดสามารถเกิดขึ้นได้ในขณะทำงาน (runtime) และ ณ เวลาใดก็ได้ ตัวนับการอ้างอิงแบบ weak จะไม่ถูกสร้างขึ้นจนกว่าจะมีการอ้างอิงแบบ weak อย่างน้อยหนึ่งรายการไปยังอ็อบเจกต์นั้น
รายการคุณสมบัติทั้งหมดที่พอยเตอร์ของเรารองรับมีดังนี้:
intrusive_ptr
: พอยเตอร์จำนวนเท่าใดก็ได้ที่สร้างขึ้นสำหรับอ็อบเจกต์เดียวกันจะใช้ตัวนับการอ้างอิงร่วมกันเพียงตัวเดียว->
): สำหรับการเข้าถึงอ็อบเจกต์ที่พอยเตอร์ชี้ไปคลาส SmartPtr
เป็นเทมเพลตและไม่มีเมธอดเสมือน (virtual methods) มันผูกพันอย่างแน่นหนากับคลาส System::Object
ซึ่งจัดการการจัดเก็บตัวนับการอ้างอิง และทำงานเฉพาะกับคลาสที่สืบทอดมาจาก System::Object
เท่านั้น
มีการเบี่ยงเบนไปจากพฤติกรรมทั่วไปของพอยเตอร์:
เพื่อรักษาความเข้ากันได้กับโค้ดเก่า ไทป์ SharedPtr
จึงกลายเป็นชื่อแฝง (alias) สำหรับ SmartPtr
ส่วนคลาส WeakPtr
ตอนนี้สืบทอดจาก SmartPtr
โดยไม่เพิ่มฟิลด์ใดๆ และเพียงแค่ทำการโอเวอร์ไรด์คอนสตรักเตอร์เพื่อให้สร้างการอ้างอิงแบบ weak เสมอ
ตอนนี้คอนเทนเนอร์จะถูกพอร์ตด้วยความหมายแบบ MyContainer<SmartPtr<MyClass>>
เสมอ และชนิดของการอ้างอิงที่จัดเก็บจะถูกเลือกในขณะทำงาน สำหรับคอนเทนเนอร์ที่เขียนขึ้นเองโดยใช้โครงสร้างข้อมูล STL (ส่วนใหญ่เป็นคอนเทนเนอร์จากเนมสเปซ System
) ไทป์การอ้างอิงเริ่มต้นจะถูกตั้งค่าโดยใช้ตัวจัดสรรหน่วยความจำ (allocator) ที่กำหนดเอง ในขณะที่ยังคงอนุญาตให้เปลี่ยนโหมดสำหรับองค์ประกอบแต่ละรายการในคอนเทนเนอร์ได้ สำหรับคอนเทนเนอร์ที่แปลแล้ว โค้ดที่จำเป็นสำหรับการสลับโหมดการจัดเก็บการอ้างอิงจะถูกสร้างขึ้นโดยตัวแปลภาษา
ข้อเสียของโซลูชันนี้ส่วนใหญ่รวมถึงประสิทธิภาพที่ลดลงระหว่างการสร้าง การคัดลอก และการลบพอยเตอร์ เนื่องจากการตรวจสอบไทป์ของการอ้างอิงที่จำเป็นถูกเพิ่มเข้าไปในการนับจำนวนการอ้างอิงตามปกติ ตัวเลขที่เฉพาะเจาะจงขึ้นอยู่กับโครงสร้างของเทสต์อย่างมาก ขณะนี้กำลังมีการหารือเกี่ยวกับการสร้างโค้ดที่เหมาะสมที่สุดในตำแหน่งที่รับประกันได้ว่าไทป์ของพอยเตอร์จะไม่เปลี่ยนแปลง
วิธีการพอร์ตของเราต้องการการวางแอตทริบิวต์ด้วยตนเองในซอร์สโค้ด C# เพื่อทำเครื่องหมายว่าตำแหน่งใดที่การอ้างอิงควรเป็นแบบ weak โค้ดที่ไม่ได้วางแอตทริบิวต์เหล่านี้อย่างถูกต้องจะทำให้เกิดหน่วยความจำรั่วไหล และในบางกรณี อาจเกิดข้อผิดพลาดอื่นๆ หลังจากการแปล โค้ดที่มีแอตทริบิวต์จะมีลักษณะดังนี้:
struct S {
MyClass s; // การอ้างอิงแบบ Strong ไปยังอ็อบเจกต์
[CppWeakPtr]
MyClass w; // การอ้างอิงแบบ Weak ไปยังอ็อบเจกต์
MyContainer<MyClass> s_s; // การอ้างอิงแบบ Strong ไปยังคอนเทนเนอร์ของการอ้างอิงแบบ Strong
[CppWeakPtr]
MyContainer<MyClass> w_s; // การอ้างอิงแบบ Weak ไปยังคอนเทนเนอร์ของการอ้างอิงแบบ Strong
[CppWeakPtr(0)]
MyContainer<MyClass> s_w; // การอ้างอิงแบบ Strong ไปยังคอนเทนเนอร์ของการอ้างอิงแบบ Weak
[CppWeakPtr(1)]
Dictionary<MyClass, MyClass> s_s_w; // การอ้างอิงแบบ Strong ไปยังคอนเทนเนอร์ที่คีย์ถูกจัดเก็บด้วยการอ้างอิงแบบ Strong และค่าด้วยการอ้างอิงแบบ Weak
[CppWeakPtr, CppWeakPtr(0)]
Dictionary<MyClass, MyClass> w_w_s; // การอ้างอิงแบบ Weak ไปยังคอนเทนเนอร์ที่คีย์ถูกจัดเก็บด้วยการอ้างอิงแบบ Weak และค่าด้วยการอ้างอิงแบบ Strong
}
ในบางกรณี จำเป็นต้องเรียกใช้คอนสตรักเตอร์แบบนามแฝง (aliasing constructor) ของคลาส SmartPtr
หรือเมธอดที่ตั้งค่าไทป์การอ้างอิงที่จัดเก็บด้วยตนเอง เราพยายามหลีกเลี่ยงการแก้ไขโค้ดที่พอร์ตแล้ว เนื่องจากการเปลี่ยนแปลงดังกล่าวจะต้องทำซ้ำใหม่ทุกครั้งหลังจากการรันตัวแปลภาษา แต่เรามุ่งมั่นที่จะเก็บโค้ดดังกล่าวไว้ภายในซอร์สโค้ด C# เรามีสองวิธีในการทำเช่นนี้:
class Service {
public static void SetWeak<T>(T arg) {}
}
class Service {
public:
template <typename T> static void SetWeak(SmartPtr<T> &arg)
{
arg.set_Mode(SmartPtrMode::Weak);
}
};
class MyClass {
private Dictionary<string, object> data;
public void Add(string key, object value)
{
data.Add(key, value);
//CPPCODE: if (key == u"Parent") data->data()[key].set_Mode(SmartPtrMode::Weak);
}
}
ในที่นี้ เมธอด data()
ใน System::Collections::Generic::Dictionary
จะส่งคืนการอ้างอิงไปยัง std::unordered_map
ที่อยู่เบื้องหลังของคอนเทนเนอร์นี้