28

First take a look at what C++ Primer said about unique_ptr and shared_ptr:
$16.1.6. Efficiency and Flexibility

We can be certain that shared_ptr does not hold the deleter as a direct member, because the type of the deleter isn’t known until run time.

Because the type of the deleter is part of the type of a unique_ptr, the type of the deleter member is known at compile time. The deleter can be stored directly in each unique_ptr object.

So it seems like that the shared_ptr does not have a direct member of deleter, but unique_ptr does. However, the top-voted answer of another question says:

If you provide the deleter as template argument (as in unique_ptr) it is part of the type and you don't need to store anything additional in the objects of this type. If deleter is passed as constructor's argument (as in shared_ptr) you need to store it in the object. This is the cost of additional flexibility, since you can use different deleters for the objects of the same type.

The two quoted paragraph are totally conflicting, which makes me confused. What's more, many people says unique_ptr is zero overhead because it doesn't need to store the deleter as member. However, as we know, unique_ptr has a constructor of unique_ptr<obj,del> p(new obj,fcn), which means that we can pass a deleter to it, so unique_ptr seems to have stored deleter as a member. What a mess!

Community
  • 1
  • 1
choxsword
  • 3,187
  • 18
  • 44
  • 7
    zero-overhead usually means it does exactly same amount of work that the method without that particular abstraction would've done. So I'd still say it's zero overhead. – Abhinav Gauniyal Apr 24 '18 at 12:31
  • @AbhinavGauniyal Here the zero-overhead is refering to size,as is pointed out in the linkage I provided in the last paragraph. – choxsword Apr 24 '18 at 12:35
  • 4
    If your type `T` needs a stateful deleter `D`. `std::unique_ptr` **still** has no overhead over a `T*` **plus it's `D`**. You need to have a valid comparison of storage. Apples to apples – Caleth Apr 24 '18 at 13:09
  • The deleter may well be (optimized down to) zero-size, and it is known at compile time if this is the case. – n. m. could be an AI Apr 24 '18 at 13:09
  • 4
    To put it another way: `sizeof(std::unique_ptr) <= (sizeof(T*) + sizeof(D))` for all `T` and `D` – Caleth Apr 24 '18 at 13:12
  • Doesn’t the question you link to already answer your question? The accepted answer explains what’s going on. – Konrad Rudolph Apr 24 '18 at 13:32
  • @KonradRudolph To be more precise,my question derives from the linkage.The accepted answer explained part of my question and the comments under that (both by Angew and Passer By) solved my question. – choxsword Apr 24 '18 at 13:38
  • @bigxiao Personally I can’t see the material difference between Angew’s answer, and [the accepted answer here](https://stackoverflow.com/a/13460653/1968). – Konrad Rudolph Apr 24 '18 at 13:58
  • Long story short https://ideone.com/wy0tjb – n. m. could be an AI Apr 24 '18 at 14:03
  • @n.m. Now include a non-empty deleter. – Konrad Rudolph Apr 24 '18 at 15:31
  • @KonradRudolph it will take some size obviously. Did you expect a miracle? Data should be stored somewhere. – n. m. could be an AI Apr 24 '18 at 15:37
  • 3
    @n.m. No, I know what happens. My comment was a gentle nudge to make your example more complete: as a rule, counter-examples are more informative than positive cases. – Konrad Rudolph Apr 24 '18 at 15:42
  • @n.m.Just as obviously, your first comment does not cover all the cases, and no amount of sarcasm will make it otherwise. – sdenham Apr 26 '18 at 12:24

4 Answers4

36

std::unique_ptr<T> is quite likely to be zero-overhead (with any sane standard-library implementation). std::unique_ptr<T, D>, for an arbitrary D, is not in general zero-overhead.

The reason is simple: Empty-Base Optimisation can be used to eliminate storage of the deleter in case it's an empty (and thus stateless) type (such as std::default_delete instantiations).

Angew is no longer proud of SO
  • 167,307
  • 17
  • 350
  • 455
  • What about the the two quoted paragraphs,which are conflicting in content? – choxsword Apr 24 '18 at 12:32
  • 4
    I don't see a conflict. The first one says that `unique_ptr` *can* store the deleter. The second one says that sometimes it does and sometimes it doesn't. – Kevin Apr 24 '18 at 12:33
  • 1
    @bigxiao "eliminating storage" is not the same as "not storing". EBO allows the empty deleter to actually be "stored" into zero bytes, in which case there is no size overhead. `std::unique_ptr` always stores its deleter, as evidenced by [`get_deleter`](http://en.cppreference.com/w/cpp/memory/unique_ptr/get_deleter) returning a reference. – Quentin Apr 24 '18 at 12:34
  • @Quentin: That returns the deleted **which would be used**. If that deleter is a `static` class member, it would incur zero overhead per object. – MSalters Apr 24 '18 at 12:38
  • @MSalters fair enough, although a `static` member implementation sounds a bit convoluted to me. – Quentin Apr 24 '18 at 12:40
  • I'm curious about how could the `unique_ptr` do something like "it can store the deleter and sometimes it doesn't".Is it by template specialization? – choxsword Apr 24 '18 at 12:44
  • @bigxiao Not in the mood to dive into standard library code, but I'm pretty sure it's always stored in an EBO-favorable way, and then EBO kicks in automatically when possible upon specialization. – Quentin Apr 24 '18 at 12:46
  • 2
    @bigxiao - It's fairly easy to get a feel for EBO, see this small test program http://coliru.stacked-crooked.com/a/4ac999ea6a6a3885 – StoryTeller - Unslander Monica Apr 24 '18 at 12:54
  • @StoryTeller Cool,but what if I pass a deleter through the constructor?To response to such circumstance I think the `unique_ptr` need to preserve another pointer for user-defined deleter,which will always take up extra bytes. – choxsword Apr 24 '18 at 13:02
  • 4
    @bigxiao - That's the beauty of it. The type of the deleter is specified in the template parameter. So if the user passes a deleter, the smart pointer need only copy initialize it's base class from it. If the base class takes no memory, it's a zero cost abstraction. If it does, it does, no skin off our back for nothing. – StoryTeller - Unslander Monica Apr 24 '18 at 13:04
  • 1
    @bigxiao See also [boost `compressed_pair`](https://www.boost.org/doc/libs/1_65_1/libs/utility/doc/html/compressed_pair.html) – Passer By Apr 24 '18 at 13:07
  • @PasserBy I'm a little confused. Since the base class is zero size which has no member object, how could it be copy initialized and become non-zero size suddenly? I thought if it's non-zero size it need to hold a member object in default. – choxsword Apr 24 '18 at 13:13
  • 3
    @bigxiao The size of an object *may be* smaller than the sum of the sizes of it's bases and data members. `sizeof(std::default_delete)` is non-zero – Caleth Apr 24 '18 at 13:17
  • 13
    tangential but worth spreading awareness early: C++20 is on track to also allow an empty **member** optimisation, via the new attribute `[[no_unique_address]]` – underscore_d Apr 24 '18 at 14:04
  • The answer is misleading. It's implying that you can only use EBO on types the standard library knows, which is not true. Non-`final` empty custom deleters can also use EBO – KABoissonneault Apr 24 '18 at 14:15
  • @KABoissonneault Reworded somewhat; is it better? – Angew is no longer proud of SO Apr 24 '18 at 14:45
  • 1
    @Angew I still don't agree with "for an arbitrary D, is not in general zero-overhead". If it requires runtime data in `unique_ptr`, it requires data when done manually, and in all the other alternatives. That's not overhead, that's just the requirements of the problem to solve. (notable exception are function names, which require storage in `unique_ptr` but not when done manually). Claims of overhead are harmful: they lead to new programmers to avoid using `unique_ptr` with custom deleters, when doing the work manually would give the same results anyway – KABoissonneault Apr 24 '18 at 15:06
  • @KABoissonneault: Storing function _names_ ?? Formally the C++ Standard allows script implementations, but everyone uses compilers. And that means you're not storing names, at most function pointers. And the thing is, with most deleters the actual call will be inlined so you're not storing a function pointer either. – MSalters Apr 24 '18 at 15:55
  • 1
    @MSalters Function names, yes, like `f` in `f(2)`, formally an identifier-expression. So yes, I meant that they would convert to function pointers. And no, I don't think any compiler removes the storage of the function pointer from `unique_ptr`, though they're allowed to. It just would not work well outside of very local contexts – KABoissonneault Apr 24 '18 at 17:31
12

The key phrase which seems to confuse you is "The deleter can be stored directly". But there's no point in storing a deleter of type std::default_delete. If you need one, you can just create one as std::default_delete{}.

In general, stateless deleters do not need to be stored, as you can create them on demand.

MSalters
  • 173,980
  • 10
  • 155
  • 350
  • 3
    WHile true, I expect most `std::unique_ptr` implementations _do_ store the deleter, they just do so using 0 bytes. – Mooing Duck Apr 24 '18 at 22:57
11

Angew's answer explained pretty thoroughly what's going on.

For those curious how things could look under the covers

template<typename T, typename D, bool Empty = std::is_empty_v<D>>
class unique_ptr
{
    T* ptr;
    D d;

    // ... 
};

template<typename T, typename D>
class unique_ptr<T, D, true> : D
{
    T* ptr;

    // ...
};

Which specializes for empty deleters and take advantage of empty base optimization.

Passer By
  • 19,325
  • 6
  • 49
  • 96
  • So if I pass a deleter through ctor,then it will instantiate the first one,otherwise it will just instantiate the second one which is zero-size overhead. Am I right? – choxsword Apr 24 '18 at 13:23
  • @bigxiao No. The deleter type is part of `std::unique_ptr`'s type. If that type is empty, the second definition (a partial specialization) will be used. Otherwise, the first one (general case). – YSC Apr 24 '18 at 13:28
  • @YSC Thank you.I seems to understand what is going on. – choxsword Apr 24 '18 at 13:35
  • 1
    @bigxiao: Since you tagged it C++11, YSC is completely correct, but not that in C++17 you'll have 'deduction guides'. That means you no longer need to specify ``. A C++17 compiler will deduce them from your arguments, and obviously will deduce `D=std::default_delete` if you don't pass a custom deleter. – MSalters Apr 24 '18 at 15:33
  • 1
    That was obviously on purpose, and I was 100% aware of that (said in a sarcastic voice). Thx @MSalters. – YSC Apr 24 '18 at 15:51
1

Brief intro:

unique_ptr can introduce some small overhead, but not because of the deleter, but because when you move from it value must be set to null where if you were using raw pointers you could leave the old pointer in bug prone but legitimate state where it still points to where it pointed before. Obviously smart optimizer can optimize, but it is not guaranteed.

Back to the deleter:

Other answers are correct, but elaborate. So here is the simplified version witout mention of EBO or other complicated terms.

If deleter is empty(has no state) you do not need to keep it inside the unique_ptr. If you need it you can just construct it when you need it. All you need to know is the deleter type(and that is one of the template arguments for unique_ptr).

For exaple consider following code, than also demonstrates simple creation on demand of a stateless object.

#include <iostream>
#include <string>
#include <string_view>

template<typename Person>
struct Greeter{
    void greet(){
        static_assert(std::is_empty_v<Person>, "Person must be stateless");
        Person p; // Stateless Person instance constructed on demand
        std::cout << "Hello " << p() << std::endl;
    }
    // ... and not kept as a member.
};

struct Bjarne{
    std::string_view operator()(){
        return "Bjarne";
    }
};

int main() {
    Greeter<Bjarne> hello_bjarne;
    hello_bjarne.greet();
}
NoSenseEtAl
  • 28,205
  • 28
  • 128
  • 277