unique_ptr自定义删除器,_Compressed_pair利用偏特化减少存储的一些设计思路
主要是利用偏特化,
如果自定义删除器是空类(没有成员变量,可以有成员函数):
_Compressed_pair会继承删除器(删除器作为基类),但_Compressed_pair里不保存删除器对象,只保存指针,所以此时,可以把unique_ptr认为是裸指针;
如果不是空类:那么会同时保存删除器对象和指针。
避免错误的使用自定义unique_ptr deleter带来不必要的开销
避免错误的使用自定义unique_ptr deleter带来不必要的开销
这世界太繁杂了,我只想守护自己的纯粹
25 人赞同了该文章
无意间看到
FOCUS:现代 C++:一文读懂智能指针595 赞同 · 12 评论文章
昂,一个unique_ptr要用40字节???第一反应是作者是不是笔误多打了个0?然而细看一下不对啊,这里应该是用64位测的,一个raw pointer是8字节,那应该不是笔误,再认真细看一下就明白了。
还是从头梳理一下吧,首先要知道unique_ptr和shared_ptr的自定义deleter方式并不一样,unique_ptr为了优化开销需要提供deleter的类型,这里仅讨论 unique_ptr。
struct FileCloserStruct {
void operator()(FILE* fp) const {
if (fp != nullptr) {
fclose(fp);
}
}
};
void FileCloserFunc(FILE* fp) {
if (fp != nullptr) {
fclose(fp);
}
}
auto FileCloserLambda = [](FILE* fp) {
if (fp != nullptr) {
fclose(fp);
}
};
int main() {
std::unique_ptr<FILE, FileCloserStruct> uptr1(fopen("test_file.txt", "w"));
std::cout << sizeof(uptr1) << std::endl;// ???
std::unique_ptr<FILE, void(*)(FILE*)> uptr2(fopen("test_file1.txt", "w"), FileCloserFunc);
std::cout << sizeof(uptr2) << std::endl;// ???
std::unique_ptr<FILE, std::function<void(FILE*)>> uptr3(fopen("test_file2.txt", "w"), FileCloserLambda);
std::cout << sizeof(uptr3) << std::endl;// ???
std::unique_ptr<FILE, decltype(FileCloserLambda)> uptr4(fopen("test_file3.txt", "w"), FileCloserLambda);
std::cout << sizeof(uptr4) << std::endl;// ???
return 0;
}
假设都是在MSVC 32位程序上测,
先来看第一个:
std::unique_ptr<FILE, FileCloserStruct> uptr1(fopen("test_file.txt", "w"));
std::cout << sizeof(uptr1) << std::endl;// 4
如果这里只使用了无状态的自定义deleter其实和raw pointer是一样大小。
稍微深入一点点这里是如何优化掉deleter大小的,以MSVC源码为例,其他思想上应该都是一样的
这个是unique_ptr内部使用_Compressed_pair来存deleter和pointer。
根据_Compressed_pair的实现可以看出来利用模板偏特化进行了判断,如果deleter是个空基类并且可以继承的话,就不需要保存这个deleter类型的成员,直接继承这个deleter类型用EBO(空基类优化)[1]从对象布局中优化掉。
第二个:
std::unique_ptr<FILE, void(*)(FILE*)> uptr2(fopen("test_file1.txt", "w"), FileCloserFunc);
std::cout << sizeof(uptr2) << std::endl;// 8
这里传入的是函数指针,阻止了EBO,需要额外的变量来保存,所以就是8了。
第三个:
std::unique_ptr<FILE, std::function<void(FILE*)>> uptr3(fopen("test_file2.txt", "w"), FileCloserLambda);
std::cout << sizeof(uptr3) << std::endl;// 48
这也是一开始抛出的问题,虽然具体数字和最上面博主测出来的不一致,但这也跟环境有关,此处不深究,重点是,它太大了!
因为std::function本来就不是lambda的原类型,std::function是通用多态函数封装器,非常强大,但是这种强大也是有代价的,直接使用sizeof(std::function<xxx>)看它也可以发现它是需要一定的内存开销的[2]。而把std::function作为类型传给了unqiue_ptr deleter时,等于在unique_ptr里把这个std::function也给存了起来,开销自然就大了...
第四个:
std::unique_ptr<FILE, decltype(FileCloserLambda)> uptr4(fopen("test_file3.txt", "w"), FileCloserLambda);
std::cout << sizeof(uptr4) << std::endl;//4
decltype直接获取lambda原类型了,同样可以进行EBO,所以也是原始指针大小。
总结:
unique_ptr自定义deleter其实用最朴素的结构体仿函数式写法就很稳了,如果想用lambda的话,请使用decltype。
参考
- ^https://en.cppreference.com/w/cpp/language/ebo
- ^https://stackoverflow.com/questions/13503511/sizeof-of-stdfunctionvoidint-type
编辑于 2021-04-24 20:02
理性发言,友善互动
2 条评论
FileCloserLambda其实是default constructable,但是c++17不允许
std::unique_ptr<FILE, decltype(FileCloserLambda)> uptr4(fopen("test_file3.txt", "w"));
也就是必须要有第二个参数FileCloserLambda。
即使在c++17里
std::unique_ptr<FILE, decltype(FileCloserLambda)> uptr4(fopen("test_file3.txt", "w"), FileCloserLambda);
FileCloserLambda也没存进unique_ptr里(size没有变大),都能EBO,为什么不能直接删掉第二个参数。
c++20里好像是允许了
std::unique_ptr<FILE, decltype(FileCloserLambda)> uptr4(fopen("test_file3.txt", "w"));
2022-05-09
需要给lambda起一个名字,好麻烦
2022-07-15
另外一篇文章
C++学习——从unique_ptr的deleter到[[no_unique_address]]
C++学习——从unique_ptr的deleter到[[no_unique_address]]
柚子厨/萝莉控/acm银
20 人赞同了该文章
首先呢,unique_ptr是有第二个参数的,不过这个参数大部分人都不怎么用。
可以自定义一个deleter
void test_uptr(){
struct A{
};
auto My_Deleter = [](A * ptr) {
std::cout<<"My Deleter\n";
};
std::unique_ptr<A,decltype(My_Deleter)> ptr1(new A(),My_Deleter);
// No viable conversion from 'unique_ptr<[...], (lambda at /Users/mryange/test/test.cpp:57:23)>' to 'unique_ptr<[...], (default)
// std::unique_ptr<A> ptr_2 = std::move(ptr1);
}
deleter的类型算在uptr的类型里面了,而shared_ptr
不是。
可以暂时理解成sptr是纯了一个类似std::function的东西作为deleter。
本文章的重点是,uptr如何保存这个deleter。
最简单的办法就是。
template<typename T ,typename Deleter>
struct Uptr{
T * ptr;
Deleter deleter;
};
但是,显然,这不《0成本抽象》。
下面是msvc的实现。
_Zero_then_variadic_args_t这一坨是用来匹配不同的构造函数的,这里可以先不管
_Compressed_pair简化的样子。
template<typename Ty1 , typename Ty2 , bool = std::is_empty_v<Ty1> && !std::is_final_v<Ty1>>
struct Compressed_pair : private Ty1{
Ty2 second ;
Ty1 & get_first() {
return *this;
}
Ty2 & get_second(){
return second;
}
};
template<typename Ty1 , typename Ty2 >
struct Compressed_pair<Ty1,Ty2,false> {
Ty1 first;
Ty2 second ;
Ty1 & get_first() {
return first;
}
Ty2 & get_second(){
return second;
}
};
这里用到了一个叫空集类优化的东西,网上的文章有很多我这里就不解释。
这里uptr把deleter放到了first,T*放到了second。
void test_Compressed_pair(){
auto f = [](){
std::cout<<"empty class f \n";
};
Compressed_pair<decltype(f), int * > pair_empty{};
static_assert(sizeof(pair_empty) == 8);
pair_empty.get_first()();
struct NonEmptyCLass
{
NonEmptyCLass(int x) : _x(x){}
int _x;
void operator ()(){
std::cout<<"non empty class \n";
}
};
NonEmptyCLass g(114514);
Compressed_pair<NonEmptyCLass, int * > pair_not_empty{g,nullptr};
static_assert(sizeof(pair_not_empty) > 8);
pair_not_empty.get_first()();
}
empty class f
non empty class
当然了,这里还有个问题就是,如果frist是个final怎么办,不能继承了啊。
中,标准提供了更好的一种写法。
template <typename Ty1, typename Ty2>
struct Compressed_no_unique_address {
static_assert(std::is_empty_v<Ty1>);
Ty2 second;
[[no_unique_address]] Ty1 first;
Ty1 &get_first() {
return first;
}
Ty2 &get_second() {
return second;
}
};
测试
void test_Compressed_no_unique_address() {
struct EmptyCLass final {
};
static_assert(sizeof(Compressed_pair<EmptyCLass, int *>) > 8);
static_assert(sizeof(Compressed_no_unique_address<EmptyCLass, int *>) == 8);
}
也比原来的写法好看。。。
文章中出现的代码Compiler Explorer - C++ (x86-64 clang 17.0.1)
原文地址:https://blog.csdn.net/tumu_C/article/details/144345559
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!