1

Follow up this question,
How can std::unique_ptr have no size overhead?

I did following code and result is somehow unexpected:

#include <memory>

#include <cstdio>

void f(void *p){
    free(p);
}

int main(){
    auto x1 = [](void *p){ free(p); };
    auto x2 = [](void *p){ f(p); };

    printf("%zu\n", sizeof(std::unique_ptr<int>                 )); //  8, expected
    printf("%zu\n", sizeof(std::unique_ptr<int, decltype(&f)>   )); // 16, expected
    printf("%zu\n", sizeof(std::unique_ptr<int, decltype(x1)>   )); //  8, unexpected
    printf("%zu\n", sizeof(std::unique_ptr<int, decltype(x2)>   )); //  8, unexpected
}

Last two types with lambda have size of 8, even they do same thing as f().

How is this made?

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
Nick
  • 9,962
  • 4
  • 42
  • 80
  • 1
    Because in versions 3 and 4 the destructor is compile time determined by definition. Also versions 3 and 4 aren't convertible to each other. In case 2, it is the version where destructor is called via function pointer. – ALX23z Nov 01 '19 at 23:33
  • so, because decltype(x1) is known, they do not "store" lambda, but call it directly in the d-tor? – Nick Nov 01 '19 at 23:37
  • Don't you mean `delete(p)` rather than `free(p)`? – einpoklum Nov 01 '19 at 23:39
  • I suppose/pretend I use malloc / free – Nick Nov 01 '19 at 23:41
  • 1
    In case 2 you can potentially modify the dtor at run-time (in fact, you must initialize it at run time). In cases 3 and 4 you cannot. – ALX23z Nov 01 '19 at 23:43

2 Answers2

1

Desugaring the lambdas and simplifying the decltypes, you actually wrote:

int main(){
    struct x1_impl {
        void operator()(void *p) { free(p); }
    };
    x1_impl x1;
    struct x2_impl {
        void operator()(void *p) { f(p); }
    };
    x2_impl x2;

    printf("%zu\n", sizeof(std::unique_ptr<int>));
    printf("%zu\n", sizeof(std::unique_ptr<int, void (*)(void*)>));
    printf("%zu\n", sizeof(std::unique_ptr<int, x1_impl>));
    printf("%zu\n", sizeof(std::unique_ptr<int, x2_impl>));
}

unique_ptr<T, Del> needs to store both a T* and Del. As x1_impl and x2_impl have no data members, they need no storage, so the last two unique_ptrs can just store a T*. Note that decltype(&f) is void (*)(void*), but decltype(x1) is an unnamed empty struct. Lambdas and function pointers are actually nothing alike. With a function pointer, the code to execute is only known at runtime, lying behind a pointer. With a lambda, the code to execute is known at compile time, and the lambda object is actually the closure, the collection of captured variables. Here, there are no such variables, so the lambdas don't need to store anything.

HTNW
  • 27,182
  • 1
  • 32
  • 60
  • Note an important part of why this is: Function pointer types could refer to many different functions with the same signature, so the specialization of `unique_ptr` can't know *which* function it will have to store. This means two `unique_ptr`s storing different functions w/the same signature can share an implementation that calls the stored function pointer. Functors (which lambdas are syntactic sugar around) are unique types, so each `unique_ptr` ends up being a unique type which can only possibly call one thing (the functor's `operator()`), making it possible to optimize away the storage. – ShadowRanger Nov 01 '19 at 23:46
1

A capture-less lambda does not need to have any subobjects; it's just a type that has an operator() overload. As such, it can be (but is not required to be) an empty type. unqiue_ptr is allowed (but not required) to optimize the way it "contains" the deleter type so that, if the deleter type is an empty class type, then it can use various techniques to make sure that this type does not take up storage within the unique_ptr instance itself.

There are several ways to do this. The unique_ptr can inherit from the type, relying on EBO to optimize away the base class. With C++20, it can just make it a member subobject, relying on the [[no_unique_address]] attribute to provide empty member optimization. In either case, the only actual storage the unique_ptr<T> needs is for the pointer to T.

By contrast, a function pointer is a function pointer. It's a fundamental type that has to have storage, because it could point to any function with that signature. A type essentially contains the member function to call as a part of the type itself; a function pointer does not. The instance of the type doesn't actually need storage to find its operator().

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982