从unique_ptr看空基类优化

我们今天从unique_ptr出发一点一点来看下空基类优化(empty class optimization,EBCO)的概念,同时可以进一步熟悉unique_ptr,tuple等。最终可以帮我我们写代码进行一些取舍和优化。

如果感兴趣还请点个赞,攒一下创作动力🙏

unique_ptr的大小

我们经常听到unique_ptr可以替代裸指针,那么我们今天从占用的空间上来进行分析。直接上代码:

struct Obj {
    int a;
    int b;
};

int main() {
    std::unique_ptr<Obj> up = std::make_unique<Obj>();
    std::cout << sizeof(up) << std::endl; // 8

    return 0;
}

我们看到unique_ptr的对象大小是一个指针大小,和裸指针是一样大的。不过unique_ptr可以指定删除器,我们增加一个删除器来试试:

void objDeleter(Obj* obj) {
    delete obj;
}

int main() {
    using Deleter = void(*)(Obj*);
    std::unique_ptr<Obj, Deleter> up{new Obj{}, objDeleter};
    std::cout << sizeof(up) << std::endl; // 16

    return 0;
}

这里unique_ptr的对象大小则变成了16字节,两个指针大小。其实这里细想也能理解,需要用一个指针来存储函数指针到最后析构的时候用。

需要注意的是如果需要传递删除器则无法使用make_unique来构造

我们看下如果使用lambda表达式来作为删除器呢?

int main() {
    auto lambdaDeleter = [](Obj* obj){
        delete obj;
    };
    using Deleter = decltype(lambdaDeleter);
    std::unique_ptr<Obj, Deleter> up{new Obj{}, lambdaDeleter};
    std::cout << sizeof(up) << std::endl; // 8

    return 0;
}

哈,这里输出是8,lambda并没有占用空间,我们去深入下unique_ptr的代码。我们会忽略一些无关的代码:

template <typename _Tp, typename _Dp = default_delete<_Tp>>
class unique_ptr {
    // ...
    __uniq_ptr_data<_Tp, _Dp> _M_t;

public:
    ~unique_ptr() {
	    // ...
	    get_deleter()(__ptr);
    }

    deleter_type& get_deleter() noexcept { 
        return _M_t._M_deleter(); 
    }
};

这里unique_ptr的大小则是它成员__uniq_ptr_data的大小,当析构是也是去调用__uniq_ptr_data中成员函数_M_deleter去析构,那么__uniq_ptr_data是如何做到能保存lambda但是不占用空间的呢?
我们再进一步去定位到具体的代码:

template <typename _Tp, typename _Dp>
class __uniq_ptr_impl
{
public:
    _Dp& _M_deleter() noexcept { return std::get<1>(_M_t); }

private:
    tuple<pointer, _Dp> _M_t;
};

template <typename _Tp, typename _Dp>
struct __uniq_ptr_data : __uniq_ptr_impl<_Tp, _Dp>
{};

__uniq_ptr_data继承自__uniq_ptr_impl,所以实现也都在__uniq_ptr_impl这里。__uniq_ptr_impl类中使用tuple来作为成员函数存储删除器。

tuple实现

那么问题就转变成了tuple保存来lambda表达式但是不占用空间(当然不仅一个参数情况下),我们写个代码来验证下我们的推论:

int main() {
    auto lambdaDeleter = [](Obj* obj){
        delete obj;
    };
    using Deleter = decltype(lambdaDeleter);
    std::tuple<int, Deleter> tp(12, lambdaDeleter);
    std::cout << sizeof(tp) << std::endl; // 4

    return 0;
}

那么tuple是如何做到的呢,我们还要再去看下tuple的代码:

template<typename... _Elements>
class tuple : public _Tuple_impl<0, _Elements...>
{
    // ...
};

template<size_t _Idx, typename... _Elements>
struct _Tuple_impl;

template<size_t _Idx, typename _Head, typename... _Tail>
    struct _Tuple_impl<_Idx, _Head, _Tail...>
    : public _Tuple_impl<_Idx + 1, _Tail...>,
      private _Head_base<_Idx, _Head> {};

template<size_t _Idx, typename _Head>
    struct _Tuple_impl<_Idx, _Head>
    : private _Head_base<_Idx, _Head> {};

我们可以知道tuple类中并不保存成员,通过其父类来进行获取成员。父类_Tuple_impl实现如上,如果只有一个模板参数则只继承_Head_base,如果多个模板参数则要先去继承_Tuple_impl,然后拿出第一个模板参数封装成_Head_base类继承。

我们上边例子中的继承结构图就是这样:

那么最终的资源存储就是在_Head_base中了:

template<typename _Tp>
using __empty_not_final
    = __conditional_t<__is_final(_Tp), false_type,
		      __is_empty_non_tuple<_Tp>>;

template<size_t _Idx, typename _Head,
    bool = __empty_not_final<_Head>::value>
struct _Head_base;

template<size_t _Idx, typename _Head>
struct _Head_base<_Idx, _Head, false>
{
    constexpr _Head_base(const _Head& __h)
    : _M_head_impl(__h) { }

    static constexpr _Head&
    _M_head(_Head_base& __b) noexcept { 
        return __b._M_head_impl; 
    }

    _Head _M_head_impl;
};

template<size_t _Idx, typename _Head>
struct _Head_base<_Idx, _Head, true>
: public _Head {
    static constexpr _Head& _M_head(_Head_base& __b) noexcept { 
        return __b;
    }
};

这里就是可以知道了,对于可以继承数据类型,这里采用继承方式保存数据。对于不可以继承类型,使用成员变量进行存储。

那么对于成员变量的存储肯定是会有实际的空间保存的。但是对于继承的形式还是需要看父类是什么样的类型。

另外当我们使用std::get时实际上通过调用_Head_base中的_M_head函数来进行获取tuple中的成员。可以看到如果是有成员变量则返回成员变量,没有的话则是返回_Head_base的对象,然后通过这个对象再进行下一步操作。

空基类优化

C++中没有内存占用为零的类型,但是C++标准却指出,在空class被用作基类的时候,如果不给它分配内存并不会导致其被存储到与其它同类型对象或者子对象相同的地址上,那么就可以不给它分配内存,这也被称为空基类优化。

对于一个lambda表达式来说,我们知道lambda类似是一个重载了()的类,对于未捕获任何值的lambda表达式来说,他的成员就是空的,他作为基类被继承时则属于空基类。

我们来看一个例子:

int main() {
    auto lambdaDeleter = [](Obj* obj){
        delete obj;
    };
    using Deleter = decltype(lambdaDeleter);

    struct DeEmpty :public Deleter {};
    struct DeNotEmpty :public Deleter {
        int a;
    };

    std::cout << sizeof(Deleter) << std::endl;  // 1
    std::cout << sizeof(DeEmpty) << std::endl;  // 1
    std::cout << sizeof(DeNotEmpty) << std::endl; // 4

    return 0;
}

如上代码,单独的Deleter大小为1字节,DeEmpty大小为1字节,DeNotEmpty大小是4字节。当一个类是空时,编译要表示有这个类,所以需要分配一个字节的大小,当继承空基类且内部有数据成员那么空基类空间大小就会被优化为0。

总结

本文通过从unique_ptr出发来看其在不同形式的删除器下的大小,从而得到unique_ptr中使用tuple来实现,进一步看tuple实现,得出对于其保存的数据来说,有成员存储和基类继承两种形式,而基类继承的形式如果是空基类的话又会对其进行优化。

我们学习了unique_ptr的部分源码,tuple的部分源码,对于数据存储我们也学习到可以使用基类继承的形式。

ref

  • 《Effecive Modern C++》
  • 《C++模板2》
  • 12
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值