new(local): 更安全、 更简单的代码, 无需智能指针模板包装器






4.65/5 (16投票s)
通过使用 new(local) 减少错误和内存泄漏
引言
指针一直以来都渴望 C++ 中提供的自动清理功能,但这项功能仅限于大型、缓慢的静态分配(自动)对象或它们的值传递数组。在本文中,我们将引入作用域的概念,同样适用于动态对象,以及作用域版本的 new/malloc,以利用这些作用域(即关联的对象/内存将在作用域结束时被销毁)。此外,您的代码(以及生成的二进制文件)将变得更小、更简单。我从我另一篇有点蹩脚的文章中获得了关于指针也应该得到自动清理的关爱,那篇文章是关于 new 操作符
struct Object : Scope::Obj {
};
Scope global;
// entering scope
void proc() {
Scope local;
char* text = strdup(local,"text"): // malloc based memory have automatic cleanup too
Object* obj = new(global) Object(); // obj will get destructor called on program exit
Object* array[1000]={0}; //sorting pointers is fast compared to array of objects
for(int i=0;i<1000;i++) {
array[i] = new(local) Object(); // creates object associated with "local" scope
}
}
// we left scope so "Scope local" and all associated object /memory
// gets destructors called/freed in proper order
为什么手动清理是错误的根源
为自动对象提供的自动清理功能节省了大量代码行,并且避免了大量大量的错误。忘记匹配所有分配和释放的错误。当异常或错误处理程序过早退出某个地方,而并非所有控制路径都包含正确的释放语句时,内存泄漏的错误。重复释放对象一到两次导致内存损坏的错误,以及由于条件语句混乱或糟糕的多线程代码导致的错误。
性能角度
因此,我们肯定想要为自动对象提供的自动清理功能。但几乎任何对性能有要求的程序都会按指针存储对象,而不是按值存储,因为重新分配、插入或排序的成本是巨大的,正如您在 此处 的基准测试中所见。那么我们不能两者兼得吗?拥有指针的性能和效率,以及自动对象在离开作用域时因自动清理而带来的安全性和简洁性?是的,我们可以,如果我们也将作用域的概念应用于动态对象。
智能指针的更简单、更快的替代方案
并不是说 STL/Boost 不重要且有用。它们在无数情况下都很有用,因为反复重新发明轮子是没有意义的。
scope.h 的一个小优点是,您不必依赖于沉重的 stl/boost 库,仅仅是为了一个简单的指针。代码的简洁性和清晰性使代码快速且避免错误。为什么?在下面的示例代码中,可以清楚地了解基本指针类型是如何工作的,而不会限制您使用任何可想象的存储方式。
// by keeping simple pointers it's always obvious what is going on
// and how efficiently.
ptr1 = ptr2; // just pointers (4-8 bytes) are copied ie MOVE semantics
*ptr1 = *ptr2; // whole objects (??? bytes) are copied ie COPY semantics
// ie you decide when you copy or move by clear non macro-encrypted syntax.
另一方面,智能指针在何时进行复制或移动是一个巨大的谜团,它们会导致大量的不必要函数调用(只需在调试器中单步调试),而以前在简单的指针情况下,每个 CPU 时钟周期只有一个指令,而且其中大多数甚至不支持数组存储
其中大多数在简单指令处调用多个函数,并且根本无法在容器中使用。有些需要最新的 c++11 编译器或大型模板库,导致可执行文件庞大。所有这些都使得代码无法被其他人使用,因为依赖 stl/boost 的库/dll 在二进制级别上不兼容,没有任何稳定接口的概念。
auto_ptr
拥有悠久而坎坷的 stl 历史,如今对于几乎任何事物来说都是一个非常糟糕的选择。它是一个具有**复制**和**移动**语义的指针,其所有权(=自动删除)无法存储在容器中。scoped_ptr
是一个没有复制和**没有移动**语义的 auto_ptr
,可以存储在容器中,并需要 boost。它增加了一层安全性。创建的内存只会存在于该作用域内,不再存在,因此您在尝试移动或传输它时会获得编译时保护。
unique_ptr
是一个**不带复制**但**带移动**语义的 auto_ptr
,可以存储在容器中,需要 C++ 11 编译器。所有权是可移动的。shared_ptr
需要 c++ 11 编译器,并且是引用计数的,即当所有引用都消失时清理。
weak_ptr
, intrusive_ptr
……whatever new ones will come.
使用智能指针,您需要包装每个指针引用,即改变所有代码为冗长且陌生的语法,并且始终担心您刚刚创建的状态机。例如,这位家伙 在排序指针数组时遇到麻烦,这本来对他来说不是问题,但现在显然不知道他创建的状态机是什么,他正在论坛上获得诸如
“...IS 规定谓词不应假设解引用的迭代器产生非 const 对象。因此,参数应按 const 引用或按值传递。IS 还规定,接受函数对象作为参数的算法可以复制这些函数对象。因此,在大多数情况下,重载的函数调用运算符将是一个 const 成员函数……”
令人难以置信的是,我们能够引入如此复杂的“简化我们的生活”的东西。
真正的解决方案是让 C++ 语言内置智能指针,而不是创建成千上万的奇怪模板来支持这个或那个。例如,让 Object *~ 成为智能指针的声明。离开作用域时将调用析构函数,并且所有复制和移动都已包含在低级指针算术中。没有成千上万的模板包装器/库/运算符。我现在正尝试在出色的 CLang/gcc 编译器中将其实现为一个有趣的实验。Visual c++ 是闭源的,所以感谢上帝,有了整个开源运动。
但在那之前,有了作用域,您只需将您的对象从 Scope::Obj
继承一行即可,仅此而已。保持代码简单易于调试,并且库/dll 可供他人使用。只需将作用域插入您的类并使用作用域的 de/allocators。您的所有数据都将被正确地解除分配,就像使用智能指针一样,即使在异常过程中构造函数失败等情况下。
struct A { Scope local; B* ptr1; B* ptr2; A() { ptr1=new(local) B(); ptr2=new(local) B();// will not allocate and will leave prematurely due to memory // exhaustion exception. But no memory leak will happen since scope // deallocates ptr1 on scope destruction as part of object // destruction that follows this constructor exception } }
所以,如果您和我一样,偏爱小巧、清晰、简单且快速的东西。如果您不喜欢为简单的事情进行混淆,并且喜欢通过快速查看源代码来了解正在发生的事情。让我们尝试一个更简单但希望功能同样强大的替代方案。
让动态堆对象的作用域诞生
那么,自动对象或全局对象的自动清理功能是如何工作的呢?嗯。它通过简单地将作用域内每个自动对象的指针添加到内部不可见的链接列表中,并在离开时按相反顺序调用每个对象的析构函数来实现。
我们能为动态对象(通过 new/malloc 动态分配的对象)做类似的事情吗?为什么不行。我们可以有一个 void 指针数组,并将各种对象类型的指针存储在一个单独的数组中。唯一的问题是调用正确的特定于对象的析构函数。不幸的是,据我所知,c++ 不允许获取析构函数的地址。所以迄今为止我找到的唯一解决方案是使用所有存储对象中的虚析构函数,这样多态性就会为我们选择正确的析构函数。
Scope.h 的实现
下面的代码片段为了文章的目的,保持简洁且格式紧凑,以便清楚地了解正在发生的事情。完整的实现以及作用域版本的 new delete strdup malloc calloc free 和作用域的线程安全版本 TScope(用于从多个线程使用全局作用域等)在 Scope.h 中,该文件与文章顶部的 Example.cpp 一起包含在 zip 文件中。我没有附加项目文件,因为你们中的大多数人都无法打开 vs2012。
struct Scope { // This is Just simple linked list
struct Obj { virtual ~Obj() {} };
Scope* prev; Obj* ptr;
Scope() : prev(0) , ptr(0) {}
~Scope() { ptr->~Obj(); free(ptr); delete prev; } // deleting all childs in reverse order on exit
};
inline void * __cdecl operator new(unsigned int size,Scope& scope)
{
void *ptr = (void *)malloc(size); // we add all new objects to this linked list
if(scope.ptr) { Scope* prev=0; prev=(Scope*)calloc(sizeof(Scope),1); *prev=scope;
scope.prev=prev; }
scope.ptr=(Scope::Obj*)ptr;
return(ptr);
};
此解决方案的局限性
在 C 语言中,Scope.h 的使用非常简单明了。您将指针与作用域关联起来,它们会自动释放。但在 C++ 中,您需要先调用析构函数。不幸的是,C++ 不支持获取析构函数的地址。所以这是一个轻微的不便。为了支持自动清理,您的对象需要从 Scope::Obj
派生。也就是说,在类声明中添加一个派生声明是一行代码的更改,比用冗长且陌生的语法替换该类的每个指针实例(如智能指针所需)要少工作和更改。
异常处理
为了让 Scope 为我们清理一切,**无论发生什么异常**,我们使用标准的异常处理语句。通常您希望在调用异常处理程序之前发生清理(smart_ptr
行为)将 Scope
对象声明在 try {}
语句内;否则,将其放在外部,清理将在异常处理程序执行后延迟。后一种情况也可能很有用。
proc() {
try {
Scope local;
Object* array[1000]={0}; //sorting pointers is fast compared to array of objects
for(int i=0;i<1000;i++) {
array[i] = new(local) Object(); // creates object associated with "local" scope
}
} catch(...) {
printf("no manual cleanup needed anymore");
}
}
在异常情况下,作为异常展开的一部分,C++ 会调用我们作用域版本的 delete,它会从作用域中注销失败的对象,因此当作用域被销毁时,该对象就不存在了(不调用 free 或析构函数)。
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include "Scope.h"
Scope global; // Deallocates/Destroys all associated objects on program exit no matter
// what exception or error in program happens
struct A : Scope::Obj {// As for supported objects. They must be derived
// from Scope::Obj to have virtual destructor as the first virtual method
// so it is first in vtable and correct call is constructed on cleanup
// In a sense we are keeping pointers to destructors as known this way
A(){ printf("\n A %x",this); }
~A(){ printf("\n ~A %x",this); }
};
void Test() {
Scope local; // all objects associated with scope will be deallocated by leaving procedure
A* array[3]={0};
for(int i=0;i<3;i++) {
array[i]=new(local) A();
}
char* text=strdup(global,"this will get deallocated on program exit");
A* a=new(global) A(); // this will get destructor called on progam exit
}
void main() {
Test();
}
输出
A 689718 A 68b110 A 68b198 A 68b220 ~A 68b198 ~A 68b110 ~A 689718 ~A 68b220
关注点
从 Scope::Obj
派生这一事实考虑到您将获得的收益,是一个小小的不足。此外,如果您想自动释放 malloc/calloc/strdup 的内存,只需使用 zip 文件中 scope.h 的作用域版本即可。当然,对于显式释放,您必须使用 free/delete 的作用域版本,以便将它们从作用域中移除,并且在离开作用域时不会再次尝试释放它们。
可能的性能改进
普通作用域包含大约 10 个对象,因此当前链表或数组之间没有性能差异。但对于大型数组,如 100000 等,使用预分配的数组是更快、更节省空间的替代方案。我将在下一个迭代中将其重写为数组,并添加可选的预分配大小参数,如 Scope scope(items)。
通过实验性开关将自动清理构建到 c/c++ 语言中
我正试图将智能指针(在离开作用域或包含对象被销毁时调用析构函数/free 的特殊指针)内置到 c 和 c++ 中。这样,低级 os c/c++ 开发人员就可以在没有大型模板库的情况下编写更小、更安全的代码。
GCC: -fsmart-pointers 开关实现状态。到目前为止,我只是在研究如何实现自动或全局对象的清理。在第一阶段,gcc 将源代码转换为所谓的 gimple 形式
struct A{ int a; ~A(){} }; int main(int argc,char**argv) { A a[12]; return 0; }
gcc -O0 --dump-tree-gimple test.cpp 生成的代码形式,其中自动对象的自动清理(生成这些指针并调用析构函数的循环)清晰可见
try { D.1868 = 0; return D.1868; } finally { { struct A * D.1866; D.1866 = &MEM[(void *)&a + 48B]; <D.1869>: if (D.1866 == &a) goto <D.1870>; else goto <D.1871>; <D.1871>: D.1866 = D.1866 + -4; A::~A (D.1866); goto <D.1869>; <D.1870>: } }
到目前为止,似乎这段代码是在 gcc\cp\init.c 中生成的,在 proc build_vec_init
中
我愿意接受新的有趣想法或建议。但最受欢迎的是 gcc 开关实现的帮助。
最新更改
- 2012年10月20日 添加了纯 C 语言版本
- 2012年10月25日 从将指针保存在链表中改为预分配数组以提高速度。