20 二月 2025

将 C# 代码移植到 C++:智能指针

在开发从C#到Java的代码转换器时,删除未使用对象不会出现问题:Java提供了与C#足够相似的垃圾回收机制,使用类的转换后的代码能够简单地编译并运行。 然而,C++则是另一回事。显然,将引用映射为原始指针无法产生预期的结果,因为这样的转换后的代码不会删除任何内容。同时,习惯于在GC环境中工作的C#开发者将继续编写创建许多临时对象的代码。

为了确保转换后的代码中对象的及时删除,我们必须在三个选项中做出选择:

  1. 对象使用引用计数——例如,通过智能指针;
  2. 使用C++的垃圾收集器实现——例如,Boehm GC
  3. 使用静态分析确定对象应删除的点。

第三个选项被立即排除:移植库中的算法复杂性被证明是不可行的。此外,静态分析还需要扩展到使用这些转换后库的客户端代码。

第二个选项也不切实际:由于我们正在移植的是库而不是应用程序,强加一个垃圾收集器会对使用这些库的客户端代码引入限制。这个方向上的实验被认为是不成功的。

因此,我们选择了最后一个选项——使用带引用计数的智能指针,这在C++中相当典型。这反过来意味着,为了解决循环引用的问题,我们需要在强引用之外使用弱引用。

智能指针的类型

有几种知名的智能指针类型:

  • shared_ptr看似是最明显的选择,但它有一个显著的缺点:即使使用enable_shared_from_this,它也会在堆上存储引用计数器,与对象分离。为引用计数器分配和释放内存是一个相对昂贵的操作。
  • intrusive_ptr在这方面更好,因为在结构内拥有未使用的4/8字节字段相比每个临时对象的额外分配开销更小。

现在,考虑以下C#代码:

class Document
{
    private Node node;
    public Document()
    {
        node = new Node(this);
    }
    public void Prepare(Node n) { ... }
}

class Node
{
    private Document document;
    public Node(Document d)
    {
        document = d;
        d.Prepare(this);
    }
}

使用intrusive_ptr时,此代码将被翻译成如下形式:

class Document : public virtual System::Object
{
    intrusive_ptr<Node> node;
public:
    Document()
    {
        node = make_shared_intrusive<Node>(this);
    }
    void Prepare(intrusive_ptr<Node> n) { ... }
};

class Node : public virtual System::Object
{
    intrusive_ptr<Document> document;
public:
    Node(intrusive_ptr<Document> d)
    {
        document = d;
        d->Prepare(this);
    }
};

这里,三个问题立即显现出来:

  1. 需要一种机制来打破循环引用,在这种情况下,使Node::document成为弱引用。
  2. 必须有一种方法将this转换为intrusive_ptr(类似于shared_from_this)。如果我们开始改变方法签名(例如,让Document::Prepare接受Node*而不是intrusive_ptr<Node>),在调用已经构造的对象或管理对象生命周期时会出现问题。
  3. 在对象构造期间将this转换为intrusive_ptr,随后引用计数减至零(例如,在Node构造函数退出Document::Prepare时发生),不能立即删除尚未有外部引用的部分构造对象。

第一个问题手动解决,因为即使是人类也常常难以确定哪个引用应该是弱引用。在某些情况下,没有明确的答案,需要更改C#代码。 例如,在一个项目中,有一对类:“打印操作”和“打印操作参数”。每个构造函数都创建了配对对象并建立了双向引用。显然,将其中一个引用变为弱引用会破坏使用场景。最终,我们决定使用[CppWeakPtr]属性,指示转换器相应的字段应该包含弱引用而不是强引用。

第二个问题如果intrusive_ptr允许从原始指针(即this)转换,则很容易解决。Boost实现提供了这一功能。

最后,第三个问题通过在构造函数中引入局部RAII保护器变量得以解决。该保护器在创建时增加当前对象的引用计数,并在销毁时减少引用计数。重要的是,在保护器内将引用计数减至零并不会删除受保护的对象。

有了这些更改,转换前后的代码大致如下所示:

class Document
{
    private Node node;
    public Document()
    {
        node = new Node(this);
    }
    public void Prepare(Node n) { ... }
}

class Node
{
    [CppWeakPtr] private Document document;
    public Node(Document d)
    {
        document = d;
        d.Prepare(this);
    }
}
class Document : public virtual System::Object
{
    intrusive_ptr<Node> node;
public:
    Document()
    {
        System::Details::ThisProtector guard(this);
        node = make_shared_intrusive<Node>(this);
    }
    void Prepare(intrusive_ptr<Node> n) { ... }
};

class Node : public virtual System::Object
{
    weak_intrusive_ptr<Document> document;
public:
    Node(intrusive_ptr<Document> d)
    {
        System::Details::ThisProtector guard(this);
        document = d;
        d->Prepare(this);
    }
};

因此,只要任何intrusive_ptr的实现满足我们的要求,并且配有配对的weak_intrusive_ptr类,就足够了。后者必须依赖位于堆外的对象引用计数器。由于创建弱引用的操作相对于创建临时对象的操作较少,将引用计数器分为强引用(在对象内)和弱引用(在对象外)在实际代码中提供了性能提升。

模板

情况变得更加复杂,因为我们需要翻译泛型类和方法的代码,其中类型参数可以是值类型或引用类型。例如,考虑以下C#代码:

class MyContainer<T>
{
    public T field;
    public void Set(T val)
    {
        field = val;
    }
}
class MyClass {}
struct MyStruct {}

var a = new MyContainer<MyClass>();
var b = new MyContainer<MyStruct>();

直接移植的方法得到以下结果:

template <typename T> class MyContainer : public virtual System::Object
{
public:
    T field;
    void Set(T val)
    {
        field = val;
    }
};
class MyClass : public virtual System::Object {};
class MyStruct : public System::Object {};

auto a = make_shared_intrusive<MyContainer<MyClass>>();
auto b = make_shared_intrusive<MyContainer<MyStruct>>();

显然,这段代码的行为与原始代码不同,因为在实例化MyContainer<MyClass>时,field对象从堆移动到MyContainer字段,破坏了引用复制语义。同时,将MyStruct结构体放置在字段中完全正确,因为它符合C#的行为。

这种情况可以通过两种方式解决:

  1. MyContainer<T>的语义过渡到MyContainer<intrusive_ptr<T>>的语义:
auto a = make_shared_intrusive<MyContainer<intrusive_ptr<MyClass>>>();
  1. 为每个模板类创建两个特化版本:一个用于类型参数是值类型的情况,另一个用于类型参数是引用类型的情况:
template <typename T, bool is_T_reference_type = is_reference_type_v<T>> class MyContainer : public virtual System::Object
{
public:
    T field;
    void Set(T val)
    {
        field = val;
    }
};

template <typename T> class MyContainer<T, true> : public virtual System::Object
{
public:
    intrusive_ptr<T> field;
    void Set(intrusive_ptr<T> val)
    {
        field = val;
    }
};

除了随着每个附加类型参数呈指数增长的冗长性,第二种方法还有一个缺点,即每个使用MyContainer<T>的上下文必须知道T是值类型还是引用类型,这通常是不希望的。例如,当我们希望最小化包含头文件的数量或完全隐藏某些内部类型的信息时。 此外,引用类型(强引用或弱引用)的选择只能针对每个容器进行一次。这意味着即使转换后的产品代码需要两种变体,也无法同时拥有强引用的List和弱引用的List

考虑到这些因素,决定使用MyContainer<intrusive_ptr<T>>或对于弱引用使用MyContainer<weak_intrusive_ptr<T>>的语义来移植MyContainer<T>。由于最受欢迎的库不提供具有所需特性的指针,我们开发了自己的实现,命名为System::SharedPtr——使用对象内引用计数器的强引用——和System::WeakPtr——使用外部引用计数器的弱引用。System::MakeObject函数负责以std::make_shared风格创建对象。

相关新闻

相关视频

相关文章