22 三月 2024

从C#到C++代码转换的规则:类成员和控制结构

在本文中,我们将探讨翻译器如何转换类成员、变量、字段、运算符和 C# 控制结构。 我们还将介绍如何使用转换器支持库将 .NET Framework 类型正确转换为 C++。

班级成员

类方法直接映射到 C++。 这也适用于静态方法和构造函数。 在某些情况下,可能会出现额外的代码—例如,模拟对静态构造函数的调用。 扩展方法和运算符被转换为静态方法并显式调用。 终结器变成析构器。

C# 实例字段成为 C++ 实例字段。 静态字段也保持不变,除非初始化顺序很重要—这是通过将此类字段转换为单例来实现的。

属性分为 getter 方法和 setter 方法,如果没有第二种方法,则仅分为一种方法。 对于自动属性,还添加了一个私有值字段。 静态属性分为静态 getter 和 setter。 使用相同的逻辑处理索引器。

事件被转换为字段,其类型对应于 System::Event 所需的专门化。 三种方法(addremoveinvoke)形式的翻译会更正确,而且还允许支持抽象和虚拟事件。 也许将来我们会采用这样的模型,但目前 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# 中不存在的默认构造函数。 堆栈变量按原样传递。 方法参数也按原样传递,只是refout参数都成为引用(幸运的是,禁止对它们进行重载)。

字段和变量的类型被替换为其 C++ 等效项。 在大多数情况下,此类等效项是由翻译器本身从 C# 源代码生成的。 库类型(包括 .NET Framework 类型和其他一些类型)由我们用 C++ 编写,是转换器支持库的一部分,随转换后的产品一起提供。var被翻译为auto,除非需要显式类型指示来消除行为差异。

此外,引用类型包装在SmartPtr中。 值类型按原样替换。 由于类型参数可以是值类型或引用类型,因此它们也可以按原样替换,但在实例化时,引用参数将包装在SharedPtr中。 因此,List<int>被翻译为List<int32_t>,但List<Object>变为List<SmartPtr<Object>>。 在某些特殊情况下,引用类型会被转换为值类型。 例如,我们的System::String实现基于 ICU 中的UnicodeString类型,并针对堆栈存储进行了优化。

为了说明这一点,我们来翻译一下下面的类:

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

控制结构

主要控制结构的相似性正中我们下怀。诸如 ifelseswitchwhiledo-whilefortry-catchreturnbreakcontinue 等运算符大多可以原封不动地转移。也许只有 switch 是个例外,它需要一些特殊处理。首先,C# 允许在字符串类型中使用它–在 C++ 中,我们会在这种情况下生成一个 if-else if 的序列。其次,最近增加了将校验表达式匹配到类型模板的功能–然而,这也很容易展开为if序列。

C++ 中不存在的构造很有趣。 因此,using运算符保证在退出上下文时调用Dispose()方法。 在 C++ 中,我们通过在堆栈上创建一个保护对象来模拟这种行为,该对象在其析构函数中调用所需的方法。 然而,在此之前,有必要捕获using主体的代码抛出的异常,并将exception_ptr存储在守卫字段中一如果Dispose()没有抛出异常,我们存储的那个将被重新抛出。 这只是一种罕见的情况,即从析构函数抛出异常是合理的并且不是错误。finally块根据类似的方案进行翻译,只是调用了一个 lambda 函数,而不是Dispose()方法,翻译器将其主体包装在该函数中。

另一个 C# 中没有的操作符是 foreach,我们不得不仿效它。起初,我们将其翻译成等价的 while ,调用枚举器的 MoveNext() 方法,这种方法虽然通用,但速度相当慢。由于 .NET 容器的大多数 C++ 实现都使用 STL 数据结构,因此我们尽可能使用其原始迭代器,将 foreach 转换为基于范围的 for。在无法使用原始迭代器的情况下(例如,容器是用纯 C# 实现的),我们会使用封装迭代器,它在内部与枚举器协同工作。以前,选择正确的迭代方法是使用 SFINAE 技术编写的外部函数的责任,现在我们已经接近在所有容器(包括翻译过的容器)中使用正确版本的 begin-end 方法。

运算符

与控制结构一样,大多数操作符(至少是算术、逻辑和赋值)不需要特殊处理。不过,有一点很微妙:在 C# 中,表达式各部分的运算顺序是确定的,而在 C++ 中,在某些情况下可能会出现未定义的行为。例如,下面的翻译代码在使用不同工具编译后会有不同的表现:

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

幸运的是,这种问题非常罕见。我们已计划教翻译员如何处理这种情况,但由于识别具有副作用的表达式的分析工作十分复杂,目前尚未实施。

然而,即使是最简单的运算符在应用于属性时也需要特殊处理。 如上所示,属性分为 getter 和 setter,翻译器必须根据上下文插入必要的调用:

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

在第一行中,替换是微不足道的。在第二行中,有必要使用 setter_add_wrap 封装器,以确保只调用一次 GetObj() 函数,并且将调用 get_Property() 的结果和字符串文字连接起来,不仅传递给 set_Property() 方法(该方法返回 void),而且进一步用于表达式中。访问索引器时也采用同样的方法。

C++ 中没有的 C# 操作符:asistypeofdefault???. 等,将使用翻译支持库函数来模拟。在需要避免重复评估参数的情况下,例如,为了不将 GetObj()?.Invoke() 展开为 GetObj() ? GetObj().Invoke() : nullptr,会使用与上图类似的方法。

成员访问操作符 (.)可以根据上下文用 C++ 中的等效操作符替换:作用域解析操作符 (::) 或 “箭头” (->)。访问结构成员时不需要这种替换。

相关新闻

相关视频

相关文章