22 3月 2024
この記事では、トランスレータがクラス メンバー、変数、フィールド、演算子、C# 制御構造をどのように変換するかを見ていきます。 また、.NET Framework 型を C++ に正しく変換するためのトランスレーター サポート ライブラリの使用についても触れます。
クラス メソッドは C++ に直接マッピングされます。 これは静的メソッドとコンストラクターにも当てはまります。 場合によっては、追加のコードが表示されることがあります。たとえば、静的コンストラクターへの呼び出しをエミュレートするためです。 拡張メソッドと演算子は静的メソッドに変換され、明示的に呼び出されます。 ファイナライザはデストラクタになります。
C# インスタンス フィールドは C++ インスタンス フィールドになります。 静的フィールドも、初期化順序が重要な場合を除き、変更されません。これは、そのようなフィールドをシングルトンとして変換することによって実装されます。
プロパティはゲッター メソッドとセッター メソッドに分割されます。2 番目のメソッドが存在しない場合は 1 つのメソッドのみに分割されます。 自動プロパティの場合、プライベート値フィールドも追加されます。 静的プロパティは、静的なゲッターとセッターに分割されます。 インデクサーは同じロジックを使用して処理されます。
イベントはフィールドに変換され、そのタイプは System::Event
の必要な特殊化に対応します。 3 つのメソッド (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
の実装は、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();
}
主要な制御構造の類似性が私たちの手に影響を及ぼしました。 if
、else
、switch
、while
、do
-while
、for
、try
-catch
、return
、break
、continue
などの演算子は、ほとんどそのまま転送される。 このリストの例外はおそらく 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# 演算子 (as
、is
、typeof
、default
、??
、?.
など) は、トランスレーター サポート ライブラリ関数を使用してエミュレートされます。引数 (たとえば、GetObj()?.Invoke()
を GetObj() ? GetObj().Invoke() : nullptr
に展開しない場合) の二重評価を避ける必要がある場合は、上記と同様のアプローチが使用されます。
メンバー アクセス演算子 (.
) は、コンテキストに応じて C++ の同等の演算子、つまりスコープ解決演算子 (::
) または「矢印」(->
) に置き換えることができます。 構造体のメンバーにアクセスする場合、このような置換は必要ありません。