22 3月 2024

C#からC++へのコード変換ルール:クラス メンバーと制御構造

この記事では、トランスレータがクラス メンバー、変数、フィールド、演算子、C# 制御構造をどのように変換するかを見ていきます。 また、.NET Framework 型を C++ に正しく変換するためのトランスレーター サポート ライブラリの使用についても触れます。

クラスのメンバー

クラス メソッドは C++ に直接マッピングされます。 これは静的メソッドとコンストラクターにも当てはまります。 場合によっては、追加のコードが表示されることがあります。たとえば、静的コンストラクターへの呼び出しをエミュレートするためです。 拡張メソッドと演算子は静的メソッドに変換され、明示的に呼び出されます。 ファイナライザはデストラクタになります。

C# インスタンス フィールドは C++ インスタンス フィールドになります。 静的フィールドも、初期化順序が重要な場合を除き、変更されません。これは、そのようなフィールドをシングルトンとして変換することによって実装されます。

プロパティはゲッター メソッドとセッター メソッドに分割されます。2 番目のメソッドが存在しない場合は 1 つのメソッドのみに分割されます。 自動プロパティの場合、プライベート値フィールドも追加されます。 静的プロパティは、静的なゲッターとセッターに分割されます。 インデクサーは同じロジックを使用して処理されます。

イベントはフィールドに変換され、そのタイプは System::Event の必要な特殊化に対応します。 3 つのメソッド (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# に存在しない既定のコンストラクターを明示的に追加することが必要になる場合があります。 スタック変数はそのまま渡されます。 メソッドの引数も、ref 引数と out 引数の両方が参照になることを除いて、そのまま渡されます (幸いなことに、それらの引数のオーバーロードは禁止されています)。

フィールドと変数の型は、C++ の同等のものに置き換えられます。 ほとんどの場合、そのような同等のものは、トランスレーター自体によって C# ソース コードから生成されます。 .NET Framework タイプやその他のライブラリ タイプは、C++ で作成され、変換された製品とともに提供されるトランスレータ サポート ライブラリの一部です。 動作の違いを滑らかにするために明示的な型の指示が必要な場合を除き、varauto に変換されます。

さらに、参照型は SmartPtr でラップされます。 値の型はそのまま代入されます。 型引数は値型または参照型のいずれかであるため、それらもそのまま代入されますが、インスタンス化されるとき、参照引数は SharedPtr でラップされます。 したがって、List<int>List<int32_t> として変換されますが、List<Object>List<SmartPtr<Object>> になります。 一部の例外的なケースでは、参照型は値型として変換されます。 たとえば、System::String の実装は、ICUUnicodeString 型に基づいており、スタック ストレージ用に最適化されています。

説明のために、次のクラスを翻訳してみましょう。

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 のシーケンスを生成します。 第 2 に、チェックされた式を型テンプレートに一致させる機能が比較的最近追加されました。ただし、これも簡単に if のシーケンスに展開できます。

C++ に存在しない構造は興味深いものです。 したがって、using 演算子は、コンテキストを終了するときに Dispose() メソッドの呼び出しを保証します。 C++ では、スタック上にガード オブジェクトを作成することでこの動作をエミュレートし、デストラクターで必要なメソッドを呼び出します。 ただし、その前に、using の本体であるコードによってスローされた例外をキャッチし、Dispose() がスローしない場合は、exception_ptr をガードのフィールドに格納する必要があります。 例外として、保存したものは再スローされます。 これは、デストラクターからの例外のスローが正当化され、エラーではない、まれなケースです。 finally ブロックは同様のスキームに従って変換されますが、Dispose() メソッドの代わりにラムダ関数が呼び出され、トランスレーターがその本体をラップします。

C# には存在せず、エミュレートする必要があるもう 1 つの演算子は、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;

幸いなことに、そのような問題は非常にまれです。 このような瞬間に対処する方法を翻訳者に教える予定ですが、副作用のある表現を特定する分析の複雑さのため、これはまだ実装されていません。

ただし、最も単純な演算子であっても、プロパティに適用する場合は特別な処理が必要です。 上に示したように、プロパティはゲッターとセッターに分割されており、トランスレーターはコンテキストに応じて必要な呼び出しを挿入する必要があります。

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

最初の行では、置き換えは簡単であることが判明しました。 2 番目の方法では、setter_add_wrap ラッパーを使用する必要があり、GetObj() 関数が 1 回だけ呼び出され、get_Property() への呼び出しと文字列リテラルを連結した結果が渡されるだけでなく、 set_Property() メソッド (void を返す) だけでなく、さらに式で使用することもできます。 インデクサーにアクセスするときにも同じアプローチが適用されます。

C++ にない C# 演算子 (asistypeofdefault???. など) は、トランスレーター サポート ライブラリ関数を使用してエミュレートされます。引数 (たとえば、GetObj()?.Invoke()GetObj() ? GetObj().Invoke() : nullptr に展開しない場合) の二重評価を避ける必要がある場合は、上記と同様のアプローチが使用されます。

メンバー アクセス演算子 (.) は、コンテキストに応じて C++ の同等の演算子、つまりスコープ解決演算子 (::) または「矢印」(->) に置き換えることができます。 構造体のメンバーにアクセスする場合、このような置換は必要ありません。

関連ニュース

関連動画

関連記事