系统编程语言Rust使用所有权范式来确保在必须释放资源时运行时零成本的编译时间。
在C 中,我们通常使用智能指针来实现隐藏管理资源分配复杂性的相同目标。但是有几个区别:
- 在Rust中,总是只有一个所有者,而C
shared_ptr
可以轻松泄漏所有权。 - 在Rust中,我们可以借用我们不拥有的参考文献,而C
unique_ptr
不能通过weak_ptr
和lock()
安全地共享。 -
shared_ptr
的参考计数是昂贵的。
我的问题是:我们如何在以下约束中模仿C 中的所有权范例:
- 任何时候只有一个所有者
- 有可能借用指针并暂时使用它,而不必担心资源不超出范围(
observer_ptr
对此是没有用的) - 尽可能多的编译时间检查。
编辑:到目前为止的评论,我们可以得出结论:
-
编译器中没有对此的编译时间支持(我希望我未知的
可能是可能的decltype
/template Magic)。在其他地方使用静态分析(Taint?) -
没有参考计数就无法获取此功能。
-
没有标准实现可以通过拥有或借用语义来区分
shared_ptr
s -
可以通过在
shared_ptr
和weak_ptr
创建包装器类型来滚动自己的滚动:-
owned_ptr
:不可仿制的,移动 - 词,封装共享_ptr,访问borrowed_ptr
-
borrowed_ptr
:可复制,封装weak_ptr
,锁定方法 -
locked_ptr
:不可仿制的移动 - 词,从锁定weak_ptr
封装
shared_ptr
-
您根本无法使用编译时间检查进行此操作。C 类型系统缺乏任何推理物体何时脱离范围,移动或被销毁的方法 - 更不用说将其变成类型的约束。
您可以做的是具有unique_ptr
的一种变体,该变体可以对付多少个"。在运行时活跃。它将返回一个智能指针,而不是get()
返回原始指针,它将在构造上增加此计数器并在破坏时减少它。如果unique_ptr
在计数非零时被销毁,至少您知道某人在某个地方做错了什么。
但是,这不是一个万无一失的解决方案。无论您多么努力防止它,总会有一些方法可以将原始指针取向基础对象,然后再进行游戏,因为该原始指针可以轻松地超过智能指针和unique_ptr
。有时甚至有必要获得原始指针,与需要原始指针的API进行互动。
此外,所有权与指针无关。。Box
/unique_ptr
允许您分配一个对象,但是与将相同对象放在堆栈(或另一个对象内部或其他任何地方)相比,它对所有权,终身时间等都没有改变。要从C 的系统中获得相同的里程,您必须进行"借用计数"。无处不在的所有对象包装器,而不仅仅是unique_ptr
s。那是不切实际的。
因此,让我们重新访问编译时间选项。C 编译器无法帮助我们,但也许绒毛可以吗?从理论上讲,如果您实现了类型系统的整个终生部分,并将注释添加到您使用的所有API(除了您自己的代码之外),则可能有效。
,但它需要对整个程序中使用的所有功能的注释。包括第三方库的私人辅助功能。以及那些没有源代码的那些。对于那些实施太复杂而无法理解的人(从生锈的经验来看,有时候安全的原因太微妙了,无法在静态的寿命模型中表达,并且必须以略有不同的方式来帮助编译器)。对于最后两个,Linter无法验证注释确实正确,因此您要回到信任程序员。此外,某些API(或者是安全的情况下的条件)在终生系统中确实不能很好地表达出来。
换句话说,这是一个完全有用的林格,这将是实质性的原始研究,并具有失败的风险。
也许有一个中间立场可以通过20%的成本获得80%的收益,但是由于您想要一个艰难的保证(老实说,我也希望那样),艰难的运气。现有的"良好实践"在C 中,通过基本上思考(和记录)Rust程序员的方式,在没有编译器援助的情况下,已经有很长的路要来最大程度地降低风险。我不确定考虑到C 状态及其生态系统是否有太大的改善。
tl; dr只是使用生锈; - )
接下来是人们试图在C 中模仿Rust所有权范式的一部分的一些示例,成功有限:
- 终身安全:防止普通悬挂。最彻底,最严格的方法,涉及该语言的几种补充,以支持必要的注释。如果努力还活着(最后一个提交是在2019年),则将此分析添加到主流编译器中可能是"借用检查"最有可能的途径。C 。讨论了Irlo。
- 借用麻烦:C 借用检查的困难
- 是否可以使用通用的C 包装器来实现Rust的所有权模型?
- C 现在2017年:乔纳森·穆勒(JonathanMüller(视频)和相关的代码,作者说:"您实际上不应该使用它,如果您需要这样的功能,则应使用Rust。
- 使用C 移动类型和II部分模仿Rust借用检查器(实际上,它更像是模拟
RefCell
,而不是借用检查器,本身就是SE)
我相信您可以通过执行一些严格的编码约定(毕竟这是您必须要做的,因为"模板魔术"无法告诉编译器不编译不使用所说的"魔术"的代码)。在我的头顶上,以下内容可能会让您...嗯... 不幸的是,我想不出任何方法来强制Rust的规则,即在有 no no 其他现有引用时,可突变的参考只能存在于系统中的任何地方。<<<<<<new
;而是使用make_unique
。这将逐步确保以锈蚀方式"拥有"堆积的物体。&&
)和/或R-Value引用到unique_ptr
s。
此外,对于任何类型的并行性,您都需要开始处理一生,而我能想到的唯一允许跨线程终身管理(或使用共享内存的跨程序终身管理)的方法是实现您自己的"少年"包装器。这可以使用shared_ptr
实现,因为在这里,参考计数实际上很重要。但是,这仍然是不必要的开销,因为参考计数块实际上具有两个参考计数器(一个指向对象的所有shared_ptr
s,另一个用于所有weak_ptr
s)。这也有点... 奇数,因为在shared_ptr
场景中,每个人带有shared_ptr
具有"平等"所有权,而在"借用Life -liftime"方案中,仅在一个线程/过程实际上应该"拥有"内存。
我认为可以通过介绍自定义包装器来添加一定程度的编译时间内省和自定义固定性跟踪所有权和借贷的课程。
以下代码是一个假设的草图,而不是生产解决方案,它需要更多的工具,例如#在不进行消毒时省略支票。在这种情况下,它使用非常天真的终身检查器来"计数" INT中的错误。static_assert
s是不可能的,因为INT不是constexpr,但是值在那里,可以在运行时进行询问。我相信这回答了您的三个约束,无论它们是堆的堆,所以我使用简单的int类型来演示这个想法,而不是一个明智的指针。
尝试在下面的main()中删除用例(以-O3在编译器资源管理器中运行以查看样板优化),您会看到警告计数器更改。
https://godbolt.org/z/pj4wmr
// Hypothetical Rust-like owner / borrow wrappers in C++
// This wraps types with data which is compiled away in release
// It is not possible to static_assert, so this uses static ints to count errors.
#include <utility>
// Statics to track errors. Ideally these would be static_asserts
// but they depen on Owner::has_been_moved which changes during compilation.
static int owner_already_moved = 0;
static int owner_use_after_move = 0;
static int owner_already_borrowed = 0;
// This method exists to ensure static errors are reported in compiler explorer
int get_fault_count() {
return owner_already_moved + owner_use_after_move + owner_already_borrowed;
}
// Storage for ownership of a type T.
// Equivalent to mut usage in Rust
// Disallows move by value, instead ownership must be explicitly moved.
template <typename T>
struct Owner {
Owner(T v) : value(v) {}
Owner(Owner<T>& ov) = delete;
Owner(Owner<T>&& ov) {
if (ov.has_been_moved) {
owner_already_moved++;
}
value = std::move(ov.value);
ov.has_been_moved = true;
}
T& operator*() {
if (has_been_moved) {
owner_use_after_move++;
}
return value;
}
T value;
bool has_been_moved{false};
};
// Safely borrow a value of type T
// Implicit constuction from Owner of same type to check borrow is safe
template <typename T>
struct Borrower {
Borrower(Owner<T>& v) : value(v.value) {
if (v.has_been_moved) {
owner_already_borrowed++;
}
}
const T& operator*() const {
return value;
}
T value;
};
// Example of function borrowing a value, can only read const ref
static void use(Borrower<int> v) {
(void)*v;
}
// Example of function taking ownership of value, can mutate via owner ref
static void use_mut(Owner<int> v) {
*v = 5;
}
int main() {
// Rather than just 'int', Owner<int> tracks the lifetime of the value
Owner<int> x{3};
// Borrowing value before mutating causes no problems
use(x);
// Mutating value passes ownership, has_been_moved set on original x
use_mut(std::move(x));
// Uncomment for owner_already_borrowed = 1
//use(x);
// Uncomment for owner_already_moved = 1
//use_mut(std::move(x));
// Uncomment for another owner_already_borrowed++
//Borrower<int> y = x;
// Uncomment for owner_use_after_move = 1;
//return *x;
}
静态计数器的使用显然是不可取的,但是不可能将static_assert用作asher_already_maved是非const。这个想法是这些静态给出了出现错误的提示,在最终的生产代码中,它们可以被#DEFEF。unique_ptr
(要执行独特的所有者)以及增强版的observer_ptr
(要获得悬挂指针的运行时例外,即,如果通过unique_ptr
维护的原始对象不超出范围)。Trilinos软件包实现了此增强的observer_ptr
,他们称其为Ptr
。我在此处实现了unique_ptr
的增强版本(我称其为 UniquePtr
):https://github.com/certik/certik/trilinos/pull/1
最后,如果您希望将对象分配分配,但仍然可以通过安全的参考文献,则需要使用Viewable
类,请在此处查看我的初始实现:https://github.com/certik/trilinos/拉/2
这应该允许您使用C ,就像Rust一样用于指针,除了在Rust中您会遇到编译时错误,而在C 中,您会得到运行时异常。另外,应该注意的是,您只在调试模式下获得运行时例外。在发布模式下,这些类不进行这些检查,因此它们的速度与生锈一样快(本质上与原始指针一样快),但是它们可以隔离。因此,必须确保整个测试套件以调试模式运行。