กฎการแปลโค้ดจาก C# ไปเป็น C++: สมาชิกคลาสและโครงสร้างการควบคุม

ในบทความนี้ เราจะสำรวจว่านักแปลของเราแปลงสมาชิกคลาส ตัวแปร ฟิลด์ ตัวดำเนินการ และโครงสร้างการควบคุม C# อย่างไร นอกจากนี้เรายังจะกล่าวถึงการใช้ไลบรารีสนับสนุนนักแปลสำหรับการแปลงประเภท .NET Framework เป็น C++ อย่างถูกต้อง

สมาชิกชั้นเรียน

วิธีการเรียนแมปโดยตรงบน C ++ นอกจากนี้ยังใช้กับวิธีการแบบคงที่และตัวสร้างด้วย ในบางกรณี รหัสเพิ่มเติมอาจปรากฏขึ้น ตัวอย่างเช่น เพื่อจำลองการเรียกไปยังตัวสร้างแบบคงที่ วิธีการขยายและตัวดำเนินการจะถูกแปลเป็นวิธีการแบบคงที่และถูกเรียกอย่างชัดเจน ผู้เข้ารอบสุดท้ายกลายเป็นผู้ทำลายล้าง

ฟิลด์อินสแตนซ์ C# กลายเป็นฟิลด์อินสแตนซ์ C++ ฟิลด์แบบคงที่ยังคงไม่เปลี่ยนแปลง ยกเว้นในกรณีที่ลำดับการเริ่มต้นมีความสำคัญ – การดำเนินการนี้จะดำเนินการโดยการแปลฟิลด์ดังกล่าวเป็นซิงเกิลตัน

คุณสมบัติจะถูกแบ่งออกเป็นเมธอด getter และเมธอด setter หรือเพียงวิธีเดียวหากไม่มีเมธอดที่สอง สำหรับคุณสมบัติอัตโนมัติ จะมีการเพิ่มช่องค่าส่วนตัวด้วย คุณสมบัติคงที่จะถูกแบ่งออกเป็น getter และ setter แบบคงที่ ตัวสร้างดัชนีได้รับการประมวลผลโดยใช้ตรรกะเดียวกัน

กิจกรรมจะได้รับการแปลเป็นช่องต่างๆ ซึ่งเป็นประเภทที่สอดคล้องกับความเชี่ยวชาญพิเศษที่จำเป็นของ System::Event การแปลในรูปแบบของสามวิธี (add, remove และ invoke) จะมีความถูกต้องมากกว่า และยิ่งกว่านั้น ยังสามารถรองรับเหตุการณ์เชิงนามธรรมและเสมือนได้ บางทีในอนาคตเราจะมาถึงโมเดลดังกล่าว แต่ในขณะนี้ตัวเลือกคลาส Event ครอบคลุมความต้องการของเราอย่างครบถ้วน

ตัวอย่างต่อไปนี้แสดงให้เห็นถึงกฎข้างต้น:

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++ (ลบโค้ดที่ไม่มีนัยสำคัญออก):

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

ตัวแปรและฟิลด์

ฟิลด์คงที่และคงที่จะถูกแปลเป็นฟิลด์คงที่ ค่าคงที่คงที่ (ในบางกรณี – constexpr) หรือเป็นวิธีการคงที่ที่ให้การเข้าถึงซิงเกิลตัน ฟิลด์อินสแตนซ์ C# จะถูกแปลงเป็นฟิลด์อินสแตนซ์ C++ ตัวเริ่มต้นที่ซับซ้อนใดๆ จะถูกย้ายไปยังตัวสร้าง และบางครั้งจำเป็นต้องเพิ่มตัวสร้างเริ่มต้นอย่างชัดเจนโดยที่ไม่มีอยู่ใน C# ตัวแปรสแต็กจะถูกส่งต่อไปตามที่เป็นอยู่ อาร์กิวเมนต์ของเมธอดจะถูกส่งต่อไปตามที่เป็นอยู่ ยกเว้นว่าทั้งอาร์กิวเมนต์ ref และ out จะกลายเป็นการอ้างอิง (โชคดีที่ห้ามไม่ให้มีการโหลดมากเกินไป)

ประเภทของฟิลด์และตัวแปรจะถูกแทนที่ด้วยค่าเทียบเท่า C++ ในกรณีส่วนใหญ่ สิ่งที่เทียบเท่ากันดังกล่าวจะถูกสร้างขึ้นโดยนักแปลเองจากซอร์สโค้ด C# ประเภทไลบรารี รวมถึงประเภท .NET Framework และอื่นๆ เขียนโดยเราในภาษา C++ และเป็นส่วนหนึ่งของไลบรารีสนับสนุนนักแปล ซึ่งมาพร้อมกับผลิตภัณฑ์ที่แปลงแล้ว var ถูกแปลเป็น auto ยกเว้นในกรณีที่จำเป็นต้องมีการระบุประเภทที่ชัดเจนเพื่อทำให้ความแตกต่างในการทำงานราบรื่นขึ้น

นอกจากนี้ ประเภทการอ้างอิงจะรวมอยู่ใน SmartPtr ประเภทค่าจะถูกทดแทนตามที่เป็นอยู่ เนื่องจากอาร์กิวเมนต์ประเภทสามารถเป็นได้ทั้งประเภทค่าหรือประเภทการอ้างอิง จึงถูกแทนที่เช่นกัน แต่เมื่อสร้างอินสแตนซ์ อาร์กิวเมนต์อ้างอิงจะถูกรวมไว้ใน SharedPtr ดังนั้น List<int> จึงถูกแปลเป็น List<int32_t> แต่ List<Object> จะกลายเป็น List<SmartPtr<Object>> ในบางกรณีพิเศษ ประเภทการอ้างอิงจะถูกแปลเป็นประเภทค่า ตัวอย่างเช่น การใช้งาน System::String ของเรานั้นอิงตามประเภท UnicodeString จาก ICU และได้รับการปรับให้เหมาะสมสำหรับพื้นที่จัดเก็บสแต็ก

เพื่อให้เห็นภาพ ลองแปลคลาสต่อไปนี้:

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

หลังจากแปลแล้ว จะใช้แบบฟอร์มต่อไปนี้ (ลบโค้ดที่ไม่มีนัยสำคัญออก):

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

โครงสร้างการควบคุม

ความคล้ายคลึงกันของโครงสร้างการควบคุมหลักส่งผลต่อมือของเรา ตัวดำเนินการเช่น if, else, switch, while, do-while, for, try-catch, return, break และ continue ส่วนใหญ่จะถูกถ่ายโอนตามที่เป็นอยู่ ข้อยกเว้นในรายการนี้อาจเป็นเพียง switch เท่านั้น ซึ่งต้องได้รับการดูแลเป็นพิเศษบางประการ ประการแรก C# อนุญาตให้ใช้กับประเภทสตริง – ในภาษา C++ เราสร้างลำดับของ if-else if ในกรณีนี้ ประการที่สอง ความสามารถที่เพิ่มขึ้นเมื่อไม่นานมานี้ในการจับคู่นิพจน์ที่เลือกกับเทมเพลตประเภท ซึ่งยังสามารถขยายเป็นลำดับของ if ได้อย่างง่ายดาย

โครงสร้างที่ไม่มีอยู่ใน C++ ถือเป็นที่สนใจ ดังนั้น ตัวดำเนินการ using จะรับประกันการเรียกใช้เมธอด Dispose() เมื่อออกจากบริบท ในภาษา C++ เราจำลองพฤติกรรมนี้โดยการสร้างอ็อบเจ็กต์ป้องกันบนสแต็ก ซึ่งจะเรียกเมธอดที่ต้องการใน destructor อย่างไรก็ตาม ก่อนหน้านั้น จำเป็นต้องจับข้อยกเว้นที่เกิดขึ้นโดยโค้ดซึ่งเป็นส่วนเนื้อความของ using และเก็บ exception_ptr ไว้ในฟิลด์ของตัวป้องกัน – หาก Dispose() ไม่ทำให้เกิดข้อยกเว้น สิ่งที่เราเก็บไว้จะถูกโยนทิ้งใหม่ นี่เป็นเพียงกรณีที่ไม่ค่อยพบนักเมื่อการโยนข้อยกเว้นจาก destructor นั้นสมเหตุสมผลและไม่ใช่ข้อผิดพลาด บล็อก finally ได้รับการแปลตามรูปแบบที่คล้ายกัน แทนที่จะใช้เมธอด Dispose() เท่านั้น ฟังก์ชัน lambda จะถูกเรียก โดยที่นักแปลพันตัวของมันไว้

โอเปอเรเตอร์อื่นที่ไม่มีอยู่ใน C# และที่เราถูกบังคับให้จำลองคือ foreach ในตอนแรก เราแปลเป็นภาษา while ที่เทียบเท่ากัน โดยเรียกเมธอด MoveNext() ของตัวแจงนับ ซึ่งเป็นวิธีสากลแต่ค่อนข้างช้า เนื่องจากการใช้งาน C++ ของคอนเทนเนอร์ .NET ส่วนใหญ่ใช้โครงสร้างข้อมูล STL เราจึงใช้ตัววนซ้ำดั้งเดิมเมื่อเป็นไปได้ โดยแปลง foreach เป็น for แบบอิงช่วง ในกรณีที่ไม่มีตัววนซ้ำดั้งเดิม (เช่น มีการใช้คอนเทนเนอร์ใน C# ล้วนๆ) จะใช้ตัววนซ้ำแบบ wrapper ซึ่งทำงานร่วมกับตัวแจงนับภายใน ก่อนหน้านี้ การเลือกวิธีการวนซ้ำที่ถูกต้องเป็นความรับผิดชอบของฟังก์ชันภายนอกที่เขียนโดยใช้เทคนิค SFINAE ตอนนี้เราใกล้จะมีเวอร์ชันที่ถูกต้องของเมธอด begin-end ในคอนเทนเนอร์ทั้งหมด รวมถึงเวอร์ชันที่แปลแล้วด้วย

ผู้ประกอบการ

เช่นเดียวกับโครงสร้างการควบคุม ตัวดำเนินการส่วนใหญ่ (อย่างน้อยก็ทางคณิตศาสตร์ ตรรกะ และการมอบหมาย) ไม่ต้องการการประมวลผลพิเศษ อย่างไรก็ตาม มีประเด็นที่ละเอียดอ่อน: ใน C# ลำดับการประเมินส่วนของนิพจน์นั้นถูกกำหนดไว้ ในขณะที่ใน C++ อาจมีพฤติกรรมที่ไม่ได้กำหนดไว้ในบางกรณี ตัวอย่างเช่น โค้ดที่แปลต่อไปนี้ทำงานแตกต่างออกไปหลังจากการคอมไพล์ด้วยเครื่องมือที่แตกต่างกัน:

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

โชคดีที่ปัญหาดังกล่าวค่อนข้างหายาก เรามีแผนที่จะสอนนักแปลให้จัดการกับช่วงเวลาดังกล่าว แต่เนื่องจากความซับซ้อนของการวิเคราะห์ที่ระบุสำนวนที่มีผลข้างเคียง จึงยังไม่ได้ดำเนินการ

อย่างไรก็ตาม แม้แต่ตัวดำเนินการที่ง่ายที่สุดก็ยังต้องมีการประมวลผลพิเศษเมื่อนำไปใช้กับคุณสมบัติ ดังที่แสดงไว้ข้างต้น คุณสมบัติจะถูกแบ่งออกเป็น getters และ setters และนักแปลจะต้องแทรกการเรียกที่จำเป็น ขึ้นอยู่กับบริบท:

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

ในบรรทัดแรกการเปลี่ยนกลายเป็นเรื่องเล็กน้อย ประการที่สอง จำเป็นต้องใช้ wrapper setter_add_wrap เพื่อให้แน่ใจว่าฟังก์ชัน GetObj() ได้รับการเรียกเพียงครั้งเดียว และผลลัพธ์ของการเชื่อมการเรียก get_Property() และสตริงลิเทอรัลจะถูกส่งผ่านไม่เพียงแต่ ไปยังเมธอด set_Property() (ซึ่งส่งคืน void) แต่ยังเพิ่มเติมเพื่อใช้ในนิพจน์ด้วย ใช้แนวทางเดียวกันนี้เมื่อเข้าถึงตัวสร้างดัชนี

ตัวดำเนินการ C# ที่ไม่ได้อยู่ใน C++: as, is, typeof, default, ??, ?. และอื่นๆ ได้รับการจำลองโดยใช้ฟังก์ชันไลบรารีการสนับสนุนนักแปล ในกรณีที่จำเป็นต้องหลีกเลี่ยงการประเมินอาร์กิวเมนต์ซ้ำซ้อน เช่น เพื่อไม่ให้เปิดเผย GetObj()?.Invoke() ลงใน GetObj() ? GetObj().Invoke() : nullptr ซึ่งเป็นแนวทางที่คล้ายกับ ใช้อันที่แสดงไว้ด้านบน

ตัวดำเนินการเข้าถึงสมาชิก (.) อาจถูกแทนที่ด้วยสิ่งที่เทียบเท่าจาก C++ ขึ้นอยู่กับบริบท: ตัวดำเนินการแก้ไขขอบเขต (::) หรือ “ลูกศร” (->) ไม่จำเป็นต้องเปลี่ยนดังกล่าวเมื่อเข้าถึงสมาชิกของโครงสร้าง

บทความที่เกี่ยวข้อง