c++c++17c++14smart-pointersunique-ptr

How can std::unique_ptr apply EBO on closure?


#include <memory>

#include <cstdio>

int main(){
    auto x1 = [](int *p){ delete(p); };

    auto ptr = std::unique_ptr<int, decltype(x1)>(new int{1},x1);

    printf("%zu\n", sizeof(ptr)); //  8, unexpected
}

the type with lambda have size of 8

Follow up this question:How std::unique_ptr have no size overhead if using lambda

And in my conclusion,EBO(Empty Base Class Optimisation) avoids increasing the object size by inheriting the empty base class

For this purpose, let’s go to Github repository for Microsoft STL implementation of unique_ptr:

and then if we go to line 1229:

https://github.com/microsoft/STL/blob/master/stl/inc/memory#L1229

You can see the following helper type:

_Compressed_pair<_Dx, pointer> _Mypair;

The implementation stores the pointer and the deleter inside a compressed pair.

Throughout the class code you can notice that unique_ptr uses the _Mypair object to reference the data and the deleter. For example in the destructor:

~unique_ptr() noexcept {
        if (_Mypair._Myval2) {
            _Mypair._Get_first()(_Mypair._Myval2); // call deleter
        }
    }

Let’s have a look at the code:

This time we have to go into the xmemory header:

https://github.com/microsoft/STL/blob/master/stl/inc/xmemory#L1487

We have two template specialisations:

The first one:

type here// store a pair of values, deriving from empty first
template <class _Ty1, class _Ty2, bool = is_empty_v<_Ty1> && 
                                         !is_final_v<_Ty1>>
class _Compressed_pair final : private _Ty1 {
public:
    _Ty2 _Myval2;
    
    // ... the rest of impl

And the second one:

// store a pair of values, not deriving from first
template <class _Ty1, class _Ty2>
class _Compressed_pair<_Ty1, _Ty2, false> final { 
public:
    _Ty1 _Myval1;
    _Ty2 _Myval2;
    
    // ... the rest of impl

the compressed pair is quite simple, as it considers only if the first type is empty.And I think std::unique_ptr<int, decltype(x1)>) use the first one,but before C++20,the Closure types have no (since C++14) default constructor.,So the _Compressed_pair in the first one derive the base class without default constructor,how can it create instance _Compressed_pair<_Dx, pointer> _Mypair;?

If the constructor of the base class is deleted, then the subclass cannot construct objects either.So how does unique_ptr apply EBO on closure before C++20?


Solution

  • If the constructor of the base class is deleted, then the subclass cannot construct objects either.

    Only the default constructor of the lambda is deleted, but it remains capable of being copy/move constructed.

        static_assert(!std::is_default_constructible_v<decltype(x1)>); // different since C++20
        static_assert(std::is_move_constructible_v<decltype(x1)>);
        static_assert(std::is_copy_constructible_v<decltype(x1)>);
    

    Demo

    _Compressed_pair provides a constructor that allows for the copy/move initialization of Ty_1.

        template <class _Other1, class... _Other2>
        constexpr _Compressed_pair(_One_then_variadic_args_t, _Other1&& _Val1, _Other2&&... _Val2) noexcept(
            conjunction_v<is_nothrow_constructible<_Ty1, _Other1>, is_nothrow_constructible<_Ty2, _Other2...>>)
            : _Ty1(_STD forward<_Other1>(_Val1)), _Myval2(_STD forward<_Other2>(_Val2)...) {}
    

    When you construct a std::unique_ptr like this, you provide x1 as an argument.

     auto ptr = std::unique_ptr<int, decltype(x1)>(new int{1},x1);
    

    That's how _Mypair is constructed.