44

I am reading "Effective Modern C++". In the item related to std::unique_ptr it's stated that if the custom deleter is a stateless object, then no size fees occur, but if it's a function pointer or std::function size fee occurs. Could you explain why?

Let's say that we have the following code:

auto deleter_ = [](int *p) { doSth(p); delete p; };
std::unique_ptr<int, decltype(deleter_)> up(new int, deleter_);

To my understanding, the unique_ptr should have an object of type decltype(deleter_) and assign deleter_ to that internal object. But obviously that's not what's happening. Could you explain the mechanism behind this using smallest possible code example?

Praetorian
  • 106,671
  • 19
  • 240
  • 328
jnbrq -Canberk Sönmez
  • 1,790
  • 1
  • 17
  • 29
  • 5
    The implementation can use the empty base class optimization on its internal data, making a stateless deleter object "disappear" by deriving some other structure from it. – Bo Persson Oct 22 '15 at 20:18
  • @BoPersson OK, I cannot figure out how that technique is used here. Could you post an answer about that? Thank you. – jnbrq -Canberk Sönmez Oct 22 '15 at 20:22
  • 2
    Ok, I have added an example. – Bo Persson Oct 22 '15 at 20:36
  • 2
    Lambdas are not, to my knowledge, stateless. But note also that lambda types are *unique*, so as long as the compiler knows the exact type of the lambda used as a deleter, if the lambda doesn't actually capture anything (which this one doesn't), it should in theory have all the information it needs *at compile time* to write the deletion code--*without* a runtime space penalty. I don't know whether that's guaranteed, though. – Kyle Strand Oct 22 '15 at 20:46
  • 3
    @KyleStrand Well, as a result of my googling efforts, I have found out that lambdas not capturing anything are stateless and others are stateful; your information that lambdas have unique types is very important, I think. – jnbrq -Canberk Sönmez Oct 22 '15 at 20:48
  • Awesome. That also explains the rationale behind making the "lambda type" known only to the compiler. I'll write up an answer. – Kyle Strand Oct 22 '15 at 20:52
  • @KyleStrand Are you suggesting the implementation can get by without storing the deleter? How would [`unique_ptr::get_deleter`](http://en.cppreference.com/w/cpp/memory/unique_ptr/get_deleter) or the [`shared_ptr`](http://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr) constructor that takes a `unique_ptr` argument work in that case? – Praetorian Oct 22 '15 at 21:00
  • @Praetorian I am indeed suggesting that. For *any* stateless object, including stateless lambdas, why would the compiler require a *specific instance* of the object in order to implement the functions you mention? – Kyle Strand Oct 22 '15 at 21:01
  • 1
    @KyleStrand I just gave you two reasons why. What if the deleter type is not default constructible? How would you create an instance and pass it to the `shared_ptr`? Also, `get_deleter` returns a reference to the deleter, not a copy. – Praetorian Oct 22 '15 at 21:05
  • @Praetorian Conceptually, the only reason you need an object in runtime memory to generate a copy of that object would be if the original object's state has some effect on the new object's initial state, or conversely (as in the case of `auto_ptr`) if the original object needs to be changed in some way when a copy is made. But for stateless objects, this can't be the case. So there's no conceptual reason for the compiler to allocate extra space for a stateless object, *even if it must copy the object*. – Kyle Strand Oct 22 '15 at 21:39
  • 1
    As for passing a reference, the `unique_ptr` implementation shown by BoPerrson below has the unique pointer implementation class inheriting from the deleter class, so the reference's underlying pointer can just be the address of the `unique_ptr` itself. There may be other possible implementations, but this at least shows that there's no logically-necessary space penalty. – Kyle Strand Oct 22 '15 at 21:39
  • 2
    @Kyle Default/copy construction can have side effects whether the type has state or not (modify global, print to stdout). I don't understand the last sentence of your first comment, seems you're saying it's possible to copy something that doesn't exist. BoPersson's answer and mine say the same thing, EBO is used to save space. But that's not the argument here, you claim captureless lambdas are somehow special, so the compiler has all the information it needs just from the template argument, there's no need to store the deleter, but I can get the same behavior from writing a stateless functor – Praetorian Oct 22 '15 at 22:00
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/93117/discussion-between-kyle-strand-and-praetorian). – Kyle Strand Oct 22 '15 at 22:15

4 Answers4

42

A unique_ptr must always store its deleter. Now, if the deleter is a class type with no state, then the unique_ptr can make use of empty base optimization so that the deleter does not use any additional space.

How exactly this is done differs between implementations. For instance, both libc++ and MSVC store the managed pointer and the deleter in a compressed pair, which automatically gets you empty base optimization if one of the types involved is an empty class.

From the libc++ link above

template <class _Tp, class _Dp = default_delete<_Tp> >
class _LIBCPP_TYPE_VIS_ONLY unique_ptr
{
public:
    typedef _Tp element_type;
    typedef _Dp deleter_type;
    typedef typename __pointer_type<_Tp, deleter_type>::type pointer;
private:
    __compressed_pair<pointer, deleter_type> __ptr_;

libstdc++ stores the two in an std::tuple and some Google searching suggests their tuple implementation employs empty base optimization but I can't find any documentation stating so explicitly.

In any case, this example demonstrates that both libc++ and libstdc++ use EBO to reduce the size of a unique_ptr with an empty deleter.

Praetorian
  • 106,671
  • 19
  • 240
  • 328
  • 2
    Note that if the pointer is an empty object of the same type as `deleter_type`, the above code will fail to compress them both. Except that only a git would do that. ;) – Yakk - Adam Nevraumont Oct 22 '15 at 21:18
  • 3
    @Deduplicator I suppose you could have an empty type that satisfies *NullablePointer* and also acts as a no-op deleter. It would probably be the most useless `unique_ptr` ever created, but Yakk's comment applies in that case :) – Praetorian Oct 22 '15 at 23:20
  • @praetorian Like I said, you'd have to be a git. ;) – Yakk - Adam Nevraumont Oct 23 '15 at 00:38
  • And the most famous self-proclaimed git, Linus Torvalds, wouldn't touch C++ with a bargepole, so the issue doesn't arise. – Steve Jessop Oct 23 '15 at 10:19
19

If the deleter is stateless there's no space required to store it. If the deleter is not stateless then the state needs to be stored in the unique_ptr itself.
std::function and function pointers have information that is only available at runtime and so that must be stored in the object alongside the pointer the object itself. This in turn requires allocating (in the unique_ptr itself) space to store that extra state.

Perhaps understanding the Empty Base Optimization will help you understand how this could be implemented in practice.
The std::is_empty type trait is another possibility of how this could be implemented.

How exactly library writers implement this is obviously up to them and what the standard allows.

SirGuy
  • 10,660
  • 2
  • 36
  • 66
  • @jnbrq-CanberkSönmez The mechanism involved is explained quite well by many people so I just linked you to some more information on the subject. Hopefully this will give you the key insights needed to visualize how this is possible to achieve in the language. – SirGuy Oct 23 '15 at 15:04
15

From a unique_ptr implementation:

template<class _ElementT, class _DeleterT = std::default_delete<_ElementT>>
class unique_ptr
{
public:
   // public interface...

private:

  // using empty base class optimization to save space
  // making unique_ptr with default_delete the same size as pointer

  class _UniquePtrImpl : private deleter_type
  {
  public:
     constexpr _UniquePtrImpl() noexcept = default;

     // some other constructors...

     deleter_type& _Deleter() noexcept
     { return *this; }

     const deleter_type& _Deleter() const noexcept
     { return *this; }

     pointer& _Ptr() noexcept
     { return _MyPtr; }

     const pointer _Ptr() const noexcept
     { return _MyPtr; }

  private:
     pointer   _MyPtr;

  };

  _UniquePtrImpl   _MyImpl;

};

The _UniquePtrImpl class contains the pointer and derives from the deleter_type.

If the deleter happens to be stateless, the base class can be optimized so that it takes no bytes for itself. Then the whole unique_ptr can be the same size as the contained pointer - that is: the same size as an ordinary pointer.

JFMR
  • 23,265
  • 4
  • 52
  • 76
Bo Persson
  • 90,663
  • 31
  • 146
  • 203
6

In fact there will be a size penalty for lambdas that are not stateless, i.e., lambdas that capture one or more values.

But for non-capturing lambdas, there are two key facts to notice:

  • The type of the lambda is unique and known only to the compiler.
  • Non-capturing lambdas are stateless.

Therefore, the compiler is able to invoke the lambda purely based on its type, which is recorded as part of the type of the unique_ptr; no extra runtime information is required.

This is in fact why non-capturing lambdas are stateless. In terms of the size penalty question, there is of course nothing special about non-capturing lambdas compared to any other stateless deletion functor type.

Note that std::function is not stateless, which is why the same reasoning does not apply to it.

Finally, note that although stateless objects are typically required to have nonzero size in order to ensure that they have unique addresses, stateless base classes are not required to add to the total size of the derived type; this is called the empty base optimization. Thus unique_ptr can be implemented (as in Bo Perrson's answer) as a type that derives from the deleter type, which, if it's stateless, will not contribute a size penalty. (This may in fact be the only way to correctly implement unique_ptr without a size penalty for stateless deleters, but I'm not sure.)

Kyle Strand
  • 15,941
  • 8
  • 72
  • 167
  • 2
    The libstdc++ approach is to use std::tuple for empty base optimization out of the box, instead of explicitly piling empty base classes everywhere. – oakad Oct 22 '15 at 23:56
  • @oakad That sounds much more elegant. – Kyle Strand Oct 23 '15 at 00:15
  • ...though most or all std::tuple implementations rely on inheritance anyway, don't they? – Kyle Strand Oct 23 '15 at 00:16
  • Yes, they are. But that's the beauty of encapsulation. – oakad Oct 23 '15 at 00:20
  • I don't think the *uniqueness* of the lambda type is an important property. A non-lambda `struct deleter { operator()(void * ptr) { ... } }` is just as empty as a lambda, and the same treatment applies – Caleth Sep 22 '17 at 08:36
  • @Caleth But there, again, you have a unique type, `deleter`. If lambdas all had the same type (as they do in, say, Python), then there would be runtime information required to determine which function to invoke when they are called. – Kyle Strand Sep 22 '17 at 18:45