27 三月 2024
有时,用C#和C++编写的代码的行为可能会有所不同。让我们仔细看看CodePorting.Translator Cs2Cpp是如何处理这些差异并确保代码转换的正确性的。我们还将学习如何进行单元测试的转换。
对于创建引用类型对象,我们使用MakeObject
函数(类似于std::make_shared
),该函数使用new
操作符创建一个对象,并立即将其包装在SharedPtr
中。使用这个函数使我们能够避免由原始指针引入的问题,但它也产生了一个访问权限问题:由于它位于所有类之外,它无法访问私有构造函数。将这个函数声明为具有非公共构造函数的类的友元,将使得在所有上下文中创建这样的类成为可能。因此,这个函数的外部版本被限制为只能与公共构造函数一起使用,并且为非公共构造函数添加了静态MakeObject
方法,这些方法具有与被代理构造函数相同的访问级别和相同的参数。
C#程序员经常在对象创建表达式中使用属性初始化器。相应的语法必须包装在lambda函数中,否则就不可能将初始化器作为单一表达式的一部分编写:
Foo(new MyClass() { Property1 = "abc", Property2 = 1, Field1 = 3.14 });
Foo([&]{ auto tmp_0 = System::MakeObject<MyClass>();
tmp_0->set_Property1(u"abc");
tmp_0->set_Property2(1);
tmp_0->Field1 = 3.14;
return tmp_0;
}());
方法调用按原样传递。在处理重载方法时,有时需要显式转换参数类型,因为C++中解决重载的规则与C#中的不同。例如,考虑以下代码:
class MyClass<T>
{
public void Foo(string s) { }
public void Bar(string s) { }
public void Bar(bool b) { }
public void Call()
{
Foo("abc");
Bar("def");
}
}
翻译后,它看起来像这样:
template<typename T>
class MyClass : public System::Object
{
public:
void Foo(System::String s)
{
CODEPORTING_UNUSED(s);
}
void Bar(System::String s)
{
CODEPORTING_UNUSED(s);
}
void Bar(bool b)
{
CODEPORTING_UNUSED(b);
}
void Call()
{
Foo(u"abc");
Bar(System::String(u"def"));
}
};
注意:Call
方法内部对Foo
和Bar
的方法调用写法不同。这是因为,如果没有对System::String
构造函数的显式调用,就会调用接受bool
的Bar
重载,因为根据 C++ 规则,这种类型转换具有更高的优先级。对于Foo
方法,没有这种歧义,翻译器生成更简单的代码。
C#和C++ 行为不同的另一个例子是模板扩展。在C#中,类型参数替换发生在运行时,并不影响泛型方法内调用的解析。在C++中,模板参数替换发生在编译时,因此必须模仿C#的行为。例如,考虑以下代码:
class GenericMethods
{
public void Foo<T>(T value) { }
public void Foo(string s) { }
public void Bar<T>(T value)
{
Foo(value);
}
public void Call()
{
Bar("abc");
}
}
class GenericMethods : public System::Object
{
public:
template <typename T>
void Foo(T value)
{
CODEPORTING_UNUSED(value);
}
void Foo(System::String s);
template <typename T>
void Bar(T value)
{
Foo<T>(value);
}
void Call();
};
void GenericMethods::Foo(System::String s)
{
}
void GenericMethods::Call()
{
Bar<System::String>(u"abc");
}
这里,重要的是在调用Foo
和Bar
时明确指定模板参数。在第一种情况下,这是必要的,因为否则,在实例化T=System::String
的版本时,会调用非模板版本,这与C#的行为不同。在第二种情况下,需要参数,因为否则它将基于字符串字面量的类型进行推断。通常,翻译器几乎总是必须明确指定模板参数以避免意外行为。
在许多情况下,翻译器必须在C#中不存在的地方生成显式调用——这主要涉及到属性和索引器的访问方法。对引用类型构造函数的调用被包装在MakeObject
中,如上所示。
在.NET中,有些方法通过params
语法支持通过参数数量和类型进行重载,通过指定object
作为参数类型,或者同时指定两者——例如,StringBuilder.Append()
和Console.WriteLine()
就存在这样的重载。这种结构的直接转换由于装箱和临时数组的创建而表现出较差的性能。在这种情况下,我们添加了一个接受任意类型的可变数量参数的重载,使用变参模板,并且我们按原样翻译参数,不进行类型转换和合并到数组中。结果,这种调用的性能得到了提高。
委托被翻译成MulticastDelegate
模板的特殊化,通常包含一个std::function
实例的容器。它们的调用、存储和赋值都是简单地进行的。匿名方法被转换成lambda函数。
在创建lambda函数时,需要延长捕获变量和参数的生命周期,这会使代码复杂化,因此翻译器只在lambda函数有可能超出周围上下文的生存期的地方这样做。这种行为(延长变量的生命周期,通过引用或值捕获)也可以手动控制以获得更优化的代码。
模仿C#在异常处理方面的行为相当不平凡。这是因为C#和C++中的异常行为不同:
这呈现了一个矛盾。如果将C#的异常类型翻译为引用类型,并通过原始指针处理它们(throw new ArgumentException
),这将导致内存泄漏或确定其删除点的重大问题。如果它们被翻译为引用类型但通过智能指针拥有(throw SharedPtr<ArgumentException>(MakeObject<ArgumentException>())
),则异常不能被其基类型捕获,因为SharedPtr<ArgumentException>
不继承自SharedPtr<Exception>
。然而,如果异常对象放在栈上,它们将被基类型正确捕获,但当保存在基类型的变量中时,关于最终类型的信息将被截断。
为了解决这个问题,我们创建了一种特殊类型的智能指针ExceptionWrapper
。其关键特性是,如果类ArgumentException
继承自Exception
,那么ExceptionWrapper<ArgumentException>
也继承自ExceptionWrapper<Exception>
。ExceptionWrapper
实例用于管理异常类实例的生命周期,截断ExceptionWrapper
类型不会导致关联的Exception
类型被截断。抛出异常由一个虚方法处理,由Exception
的后代重写,该方法创建一个由最终异常类型参数化的ExceptionWrapper
并抛出它。虚拟性质允许抛出正确类型的异常,即使ExceptionWrapper
类型之前被截断,异常对象与ExceptionWrapper
之间的链接防止了内存泄漏。
我们框架的一个优势是能够翻译不仅是源代码,还有它的单元测试
C#程序员使用NUnit和xUnit框架。翻译器将相应的测试示例转换为GoogleTest,替换检查的语法并调用标有Test
或Fact
标志的方法,从各自的测试函数中。支持无参数测试和像TestCase
或TestCaseData
这样的输入数据。下面提供了一个翻译后的测试类的示例。
[TestFixture]
class MyTestCase
{
[Test]
public void Test1()
{
Assert.AreEqual(2*2, 4);
}
[TestCase("123")]
[TestCase("abc")]
public void Test2(string s)
{
Assert.NotNull(s);
}
}
class MyTestCase : public System::Object
{
public:
void Test1();
void Test2(System::String s);
};
namespace gtest_test
{
class MyTestCase : public ::testing::Test
{
protected:
static System::SharedPtr<::ClassLibrary1::MyTestCase> s_instance;
public:
static void SetUpTestCase()
{
s_instance = System::MakeObject<::ClassLibrary1::MyTestCase>();
};
static void TearDownTestCase()
{
s_instance = nullptr;
};
};
System::SharedPtr<::ClassLibrary1::MyTestCase> MyTestCase::s_instance;
} // namespace gtest_test
void MyTestCase::Test1()
{
ASSERT_EQ(2 * 2, 4);
}
namespace gtest_test
{
TEST_F(MyTestCase, Test1)
{
s_instance->Test1();
}
} // namespace gtest_test
void MyTestCase::Test2(System::String s)
{
ASSERT_FALSE(System::TestTools::IsNull(s));
}
namespace gtest_test
{
using MyTestCase_Test2_Args = System::MethodArgumentTuple<decltype(
&ClassLibrary1::MyTestCase::Test2)>::type;
struct MyTestCase_Test2 : public MyTestCase, public ClassLibrary1::MyTestCase,
public ::testing::WithParamInterface<MyTestCase_Test2_Args>
{
static std::vector<ParamType> TestCases()
{
return
{
std::make_tuple(u"123"),
std::make_tuple(u"abc"),
};
}
};
TEST_P(MyTestCase_Test2, Test)
{
const auto& params = GetParam();
ASSERT_NO_FATAL_FAILURE(s_instance->Test2(std::get<0>(params)));
}
INSTANTIATE_TEST_SUITE_P(, MyTestCase_Test2,
::testing::ValuesIn(MyTestCase_Test2::TestCases()));
} // namespace gtest_test