将 C++ 接口写入动态分配的 C 结构

Writing a C++ interface to a dynamically allocated C struct

简介:我正在编写一个 C++11 应用程序,它广泛使用了遗留的 C 代码库。遗留代码中一个非常常见的模式是存在一些 struct LegacyStruct ,它们由

等方法构造和销毁
build_struct(LegacyStruct *L, int arg1, int arg2)
free_struct(LegacyStruct *L)

基本上就是constructor/destructor。遗留代码库中的所有权模型非常 unique_ptr-esque,所以我的目标是将其包装在内存安全的 RAII-minded 包装器 class 中,如下所示:

class Wrapper {
public:
    Wrapper::Wraper() : handle() {}
    Wrapper::Wrapper(int same_arg1, int same_arg2);
    Wrapper::Wrapper(const Wrapper &W) = delete;
    Wrapper::Wrapper(Wrapper &&W) : handle(std::move(W.handle)) {}
    //copy operator= and move operator= analogously
private:
    std::unique_ptr<LegacyStruct, custom_deleter> handle;

其中 custom_deleter 按照 this question 的方式调用 free_struct,或者只是 std::default_deleteLegacyStruct 的部分特化。反正到目前为止一切顺利,我认为这是一种常见的设计模式,很适合我的需要。

我的问题:我在处理形式为

的链表类型结构的情况下无法调整此模式
typedef struct LegacyNode {
    int stack_allocated_data;
    OtherStruct *heap_allocated_data;
    LegacyNode *next;
} LegacyNode;

同样,遗留代码库中的所有权模型是 unique_ptr 式的:链表的所有权是唯一的,即负责适当地释放它。类似地,有一个相应的 free_node(LegacyNode *N) 函数可以释放 heap_allocated_data,如果需要的话,然后释放节点本身。

施工情况却大不相同。会有一个看起来像

的函数
build_list(LegacyNode **L, int *count_p, int other_args){
    LegacyNode *newnode;

    //code allocating newnode and populating its fields

    //...and then:
    newcut->next = *L;
    *L = newcut;
    (*count_p)++;
}

build_list 的调用看起来像

int list_count = 0;
LegacyNode *L = (LegacyNode *) NULL;

build_list(&L, &list_count, 99);

Edit/clarification: build_list 是代码库中的静态非导出函数,我通过调用其他调用 build_list 可能有好几次。

因此,我会喜欢写一个ListWrap class,它存储一个头节点和一个列表长度,并且有copy/move与上面 Wrapper 相同的运算符,即列表本身具有唯一所有权,可以移动但不能复制等。

但是,我的理解是智能指针在这种情况下不是一个选项。使用 head_node 作为 LegacyNode 的智能指针,我必须将 &head_node.get() 传递给 build_list,这会破坏智能指针 invariants/ownership?

就目前而言,我的包装器 class 包含一个指向头节点的原始指针,该方法 returns 头节点的地址供 build_list 使用,一个析构函数它遍历列表调用 free_node,基于谓词的 erase 类型的方法只删除某些元素。

当然,修改和清除链表是 CS-101 级别的东西,但我仍然设法浪费了几个小时来编写它并且到处都是内存泄漏!此外,遗留代码库中还有其他几个使用几乎相同的链表结构,所以我希望能够将它变成一个 class 模板,它可以专门用于类型和删除器,并继承自提供特定于类型的方法。

谢谢

However, my understanding is that smart pointers are not an option in this case. With head_node as some smart pointer to a LegacyNode, I would have to pass &head_node.get() to build_list, which would corrupt the smart pointer invariants/ownership?

是的,这是正确的,因为 build_list 会覆盖该内存位置的对象,从而破坏智能指针的内存。但是还有一种方法,你可以用一个已有的指针构造一个std::unique_ptr

因此,不是 ListWrap 分配它自己的对象,而是 build_list 分配对象,然后只获取指针并用 RAII 将它们包装起来。

class ListWrap {
public:
    ListWrap(LegacyNode* head, int count);
    //...
private:
    std::unique_ptr<LegacyNode, &free_node> handle;
    int count;
};

ListWrap::ListWrap(LegacyNode* head, int count) : handle{ head }, count{ count } {}

这是一堆节点:

struct Nodes {
  struct DeleteAllNodes {
    void operator()(LegacyNode* node)const {
      while (auto cur = node) {
        node = cur->next;
        free_node(node);
      }
    }
  };
  std::unique_ptr<LegacyNode, DeleteAllNodes> m_nodes;
};

这里是一些操作。他们中的大多数人都会管理事情,除了简短的 windows 我评论过:

void push_node( Nodes& nodes, int other_args ) {
  int unused = 0;
  auto* tmp = nodes.m_nodes.get();
  build_list( &tmp, &unused, other_args );
  nodes.m_nodes.release(); // unmanaged
  nodes.m_nodes.reset(tmp); // everything managed now
}
Nodes pop_node( Nodes& nodes ) {
  if (!nodes.m_nodes) return {};
  auto* tmp = nodes.m_nodes->next; // unmanaged
  nodes.m_nodes->next = nullptr;
  Nodes retval;
  retval.m_nodes.reset(tmp); // everything managed now
  std::swap( retval.m_nodes, nodes.m_nodes );
  return retval;
}
void move_single_node( Nodes& dest, Nodes& src ) {
  Assert(src.m_nodes);
  if (!src.m_nodes) return;
  Nodes to_push = pop_node(src);
  LegacyNode** next = &(to_push.m_nodes->next);
  Assert(!*next); // shouldn't be possible, pop_node returns a single node
  *next = dest.m_nodes.release(); // unmanaged for a short period
  dest = std::move(to_push);
}
Nodes splice( Nodes backwards, Nodes forwards ) {
  while(backwards.m_nodes) {
    move_single_node( forwards, backwards );
  }
  return forwards;
}
template<class F>
void erase_if( Nodes& nodes, F&& f, Nodes prefix={} ) {
  if (!nodes.m_nodes) {
    return splice( std::move(prefix), std::move(nodes) );
  }
  Nodes tmp = pop_node( nodes );
  if ( !f(*tmp.m_nodes) ) {
    prefix = splice( std::move(tmp), prefix );
  }
  erase_if( nodes, std::forward<F>(f), std::move(prefix) );
}

Nodes&为第一个参数的可以是Nodes的方法。