shared_ptr with deleter class - why deleter is copied?
Asked Answered
B

3

9

Suppose I create a shared pointer with a custom deleter. In the following code I would like to check what happens with the deleter object itself:

struct A {
    A() { std::cout << "A\n"; }
    ~A() { std::cout << "~A\n"; }
};

struct D {
    D() {
        std::cout << "D\n";
    }
    ~D() {
        std::cout << "~D\n";
    }
    D(const D&) {
        std::cout << "D(D&)\n";
    }
    void operator()(A* p) const {
        std::cout << "D(foo)\n";
        delete p;
    }
};

int main()
{
    std::shared_ptr<A> p(new A, D());
}

I see that D(const D&) and ~D() for the "deleter" class are called six more times:

D
A
D(D&)
D(D&)
D(D&)
D(D&)
D(D&)
D(D&)
~D
~D
~D
~D
~D
~D
D(foo)
~A
~D

What happens? Why it needs to be copied so many times?

Barbital answered 18/12, 2019 at 3:39 Comment(2)
I can’t be bothered to look at the actual implementation (which library are you using?) but the deleter is presumably passed around by value quite a lot, since it’s expected to be a simple function pointer (or similar), and thus dirt cheap to copy. It’s still an interesting question: I would expect there to be a few copies but maybe not quite that many.Perdita
Your question is valid, but if you are concerned with performance, may be you shouldn't use shared_ptr at all, as it is the worst smart pointer in terms of performance. weak_ptr is the only reason to ever use shared_ptr.Epigeous
E
1

I checked your code with gcc 7.4 and I get the same number of calls to the destructors. What you observe is that the deleter object is moved six times through std::move(deleter).

As you have added a destructor to your class, the automatic generation of the default move semantics is disabled and you need to define them explicitly:

D(D&&) = default;
D& operator=(D&&) = default;

However, even with move semantics the destructor can still be called up to six times.

Eldrid answered 19/12, 2019 at 15:57 Comment(0)
A
0

The deleter is usually an empty class with an operator(). In optimized code and a well-written implementation of shared_ptr, deleters that are empty classes do not occupy space and, therefore, have zero copy overhead.

Implementations typically know whether copying around objects is expensive after the optimizer has done its work and whether precautions must be taken.

In the case presented, I guess you are observing an unoptimized build. You see how the implementation passes the deleter through several layers of function calls. In an optimized build, nothing will actually be copied as the deleter class D is empty.

Aronow answered 19/12, 2019 at 13:12 Comment(3)
“nothing will actually be copied as the deleter class D is empty” — I can’t think of a situation where the class being empty will impact the optimiser‘s decision whether to copy (in fact, if anything the opposite should be the case: it’s much more worthwhile to avoid costly copies of big objects than of empty ones). And while copying can be elided in general, for OP’s case it isn’t: -O2 doesn’t change the output for either clang or GCC.Perdita
@KonradRudolph It is not the decision whether to copy; it is the fact that there is nothing to copy when the deleter is an empty class. It does not matter how often it must be copied when there is nothing to copy.Aronow
The issue is that it’s still invoking a copy constructor, even though the class is empty. That is the expensive part, if the copy constructor is not defaulted. In fact, that’s why copy elision is explicitly permitted by the standard in some situations. Your answer speculates that OP is seeing an unoptimised build. I’m telling you, specifically, that the same result is observed in an optimised build. No copy elision happens here, despite the fact that the class is empty (because, as I’ve said, whether a class is empty has no bearing on whether to elide copies).Perdita
S
0

Its inquisitive to create your custom deleter but on the other hand it lacks optimization as a well-defined shared pointer would run to implement the functionality of delete. (+new)

If you look at the implementation of shared_ptr in <memory>, you can get a knack of how to prepare your deleter.

Note that implementation of smart pointers varies in accordance with the compiler and the library used. (for example your code gave me 3 copies)

Still investigating as to why this occurs but yes a weak_ptr is a better choice as it breaks the reference cycles formed by objects managed by shared_ptr.

Spermatozoid answered 19/12, 2019 at 14:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.