22 3월 2024
이 글에서는 번역기가 클래스 멤버, 변수, 필드, 연산자 및 C# 제어 구조를 변환하는 방법을 살펴봅니다. 또한 .NET Framework 유형을 C++로 올바르게 변환하기 위한 번역기 지원 라이브러리의 사용법에 대해서도 다룰 것입니다.
클래스 메서드는 C++에 직접 매핑됩니다. 이는 정적 메서드와 생성자에도 적용됩니다. 경우에 따라 정적 생성자에 대한 호출을 에뮬레이트하기 위한 추가 코드가 나타날 수 있습니다(예: 정적 생성자 호출). 확장 메서드와 연산자는 정적 메서드로 변환되어 명시적으로 호출됩니다. 파이널라이저는 소멸자가 됩니다.
C# 인스턴스 필드는 C++ 인스턴스 필드가 됩니다. 정적 필드도 초기화 순서가 중요한 경우를 제외하고는 변경되지 않으며, 이러한 필드를 싱글톤으로 변환하여 구현합니다.
프로퍼티는 게터 메서드와 세터 메서드로 분할되거나, 두 번째 메서드가 없는 경우 하나만 분할됩니다. 자동 프로퍼티의 경우 비공개 값 필드도 추가됩니다. 정적 프로퍼티는 정적 게터와 세터로 나뉩니다. 인덱서는 동일한 논리를 사용하여 처리됩니다.
이벤트는 필드로 번역되며, 그 유형은 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
구현은 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
시퀀스를 생성합니다. 둘째, 체크된 표현식을 유형 템플릿에 일치시키는 기능이 비교적 최근에 추가되었습니다. 그러나 이 기능은 ‘if’ 시퀀스로 쉽게 전개됩니다.
C++ 에는 존재하지 않는 구조체가 흥미롭습니다. 따라서 using
연산자는 컨텍스트를 종료할 때 Dispose()
메서드의 호출을 보장합니다. C++에서는 스택에 가드 객체를 생성하여 해당 소멸자에서 필요한 메서드를 호출함으로써 이 동작을 에뮬레이트합니다. 그러나 그 전에 using
의 본문이었던 코드가 던진 예외를 포착하고 가드 필드에 exception_ptr
을 저장해야 하는데, Dispose()
가 예외를 던지지 않으면 우리가 저장한 예외가 다시 던져지게 됩니다. 이것은 소멸자에서 예외를 던지는 것이 정당화되고 오류가 아닌 드문 경우일 뿐입니다. finally
블록은 비슷한 체계에 따라 변환됩니다. 단, Dispose()
메서드 대신 람다 함수가 호출되어 변환기가 해당 본문을 래핑합니다.
C#에 없고 강제로 에뮬레이트해야 하는 또 다른 연산자는 foreach
입니다. 처음에는 열거자의 MoveNext()
메서드를 호출하여 이를 동등한 while
으로 변환했는데, 이는 보편적이지만 매우 느립니다. .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# 연산자: as
, is
, typeof
, default
, ??
, ?.
등은 변환기 지원 라이브러리 함수를 사용하여 에뮬레이트됩니다. 예를 들어 GetObj()?.Invoke()
를 GetObj() ? GetObj().Invoke() : nullptr
로 전개하지 않으려는 등 인수의 이중 평가를 피해야 하는 경우에는 다음과 유사한 접근 방식을 사용합니다. 위에 표시된 것이 사용됩니다.
멤버 액세스 연산자(.
)는 문맥에 따라 범위 분해 연산자(::
) 또는 “화살표”(->
)와 같은 C++ 의 연산자로 대체할 수 있습니다. 구조체의 멤버에 액세스할 때는 이러한 대체가 필요하지 않습니다.