-1

AFAIK unique_ptr is quite tricky to use with PIMPL, since deleter is part of unique_ptr type so it will not work with incomplete types. On the other hand shared_ptr uses dynamic deleter so it works with incomplete types.

shared_ptr has the performance issue of giving me an atomic operations no matter if I need it or not.

Are there any other faster alternatives in std:: I can use? I am obviously fine with type erasure, I am talking about cost of atomic refcount.

#include <any>
#include <memory>
#include <iosfwd>

std::shared_ptr<std::fstream> sp;
// unique_ptr requires complete type
// std::unique_ptr<std::fstream> up;
std::any a;

#include <fstream>
int main() {
    // any requires copy_constructible
    // a = std::fstream{};  
    sp = std::make_shared<std::fstream>();
} 

notes:

  • I considered any, but it does not work for some types.
  • I considered unique_ptr with dynamic deleter, but AFAIK unique_ptr constructor will never "tell" the deleter what is the constructed object(to give a way to deleter to learn how to destroy the object).

P.S. I know long time ago boost::shared_ptr had macro to disable atomic refcount, but even if that is still supported I do not want to switch to boost::shared_ptr.

NoSenseEtAl
  • 28,205
  • 28
  • 128
  • 277
  • 6
    `AFAIK unique_ptr is quite tricky to use with PIMPL` how do you define tricky? You just need to declare the destructor and methods that need to know the complete type in the header, and define them at the place where you define the incomplete type using `= default` ( [C++ Pimpl Idiom Incomplete Type using std::unique_ptr](https://stackoverflow.com/questions/28786387/c-pimpl-idiom-incomplete-type-using-stdunique-ptr) ) – t.niese Dec 02 '21 at 15:13
  • 1
    definition of tricky for purposes of this question: I can not just include fwd declaration header and write "normal"(beside the fact I must use pointers, not values) code, I need to make a helper struct myself. – NoSenseEtAl Dec 02 '21 at 15:15
  • 2
    I don't fully understand the problem or what you mean with `(beside the fact I must use pointers, not values) code, I need to make a helper struct myself.`. What is the problem with the following code? `/*header*/struct Type { Type(); ~Type(); struct TypeImpl; std::unique_ptr impl }; /*source*/ struct TypeImpl{}; Type::Type() : impl(new Type::TypeImpl()) {} Type::~Type() = default;` – t.niese Dec 02 '21 at 15:19
  • @t.niese As a note, if you want it to be movable, I believe you also have to add the move operators explicitly, since the user defined destructor still inhibits the move operations. But I've done what you suggest many times before, and it does work. – Dave S Dec 02 '21 at 15:27
  • I do find shared_ptr version much more simpler and readable... only downside is that I pay for atomic refcount. – NoSenseEtAl Dec 02 '21 at 15:27
  • @NoSenseEtAl: If you're fine with using `unique_ptr`, then your type is clearly non-copyable. And `shared_ptr`'s reference counter only increments when you copy it. So... what does an atomic reference count matter if you *never* increment it? This sounds like a complete micro-optimization; the cost of having every function call go through two pointer indirections (aka: the cost of doing PIMPL) will likely hurt far more than the cost of a reference count. Especially one you never use. – Nicol Bolas Dec 02 '21 at 15:59
  • If you want PIMPL, you want `std::unique_ptr`. It is simple, and it has worked for many projects over many years. Is it possible for you to provide examples of how it does not work? – Stephen M. Webb Dec 02 '21 at 16:02
  • @t.niese: That code doesn't compile. `Type::TypeImpl` also can't be implemented somewhere, because nested classes have to be part of the class definition. Maybe you meant to put that declaration outside of the definition of `Type`. – Nicol Bolas Dec 02 '21 at 16:02
  • @NoSenseEtAl: "*I am obviously fine with type erasure, I am talking about cost of atomic refcount.*" The cost of type erasure is often a *memory allocation*. The cost of an atomic reference count is a *rounding error* next to that. Before trying to optimize things, you really should understand their relative costs. – Nicol Bolas Dec 02 '21 at 16:03
  • @NicolBolas I think I can not avoid type erasure in any way with pimpl, but I think I can avoid using atomic refcount since it is totally unrelated to the fact that unique_ptr has a static deleter and shared_ptr has dynamic one. In other words: C++11 picked some points in design space and gave them names unique_ptr, shared_ptr... but space of design options is much larger, e.g. unique_ptr with dynamic deleter, shared_ptr without atomic refcount... – NoSenseEtAl Dec 02 '21 at 16:19
  • @NicolBolas defining `Type::TypeImpl` [out of line](http://coliru.stacked-crooked.com/a/5d426123b11a8f2f) is fine, so long as you declare it in the definition of `Type` – Caleth Dec 02 '21 at 16:21
  • @NoSenseEtAl: "*I think I can not avoid type erasure in any way with pimpl*" And yet, people do it every day. In fact, I don't think I've ever seen any PIMPL code that uses type erasure just to do PIMPL. – Nicol Bolas Dec 02 '21 at 16:21
  • @NoSenseEtAl: "*space of design options is much larger*" Yes, and most of those are exceedingly special cased. Consider your example, where you're fretting over an "atomic reference count" that you will *never actually use*. That right there is a good reason for the standard to *not* include such types: it encourages people to stop caring about details that don't matter. Note that your concern is not that you're going to have this extra allocation for the storage block or extra memory for one that you don't need; it's *purely* about way the unused reference count gets incremented. – Nicol Bolas Dec 02 '21 at 16:24
  • If you can get your hands on a copy of Scott Meyers's "Effective Modern C++", the PIMPL idiom is covered pretty comprehensively in Item 22, and addresses the differences between using unique_ptr and shared_ptr. – DS_London Dec 02 '21 at 18:59
  • @NicolBolas yes you are right I forgot the `Type::` in front of `TypeImpl`. – t.niese Dec 02 '21 at 19:04
  • 2
    @NoSenseEtAl my concern would be more maintainability, and bug prevention. Using `std::shared_ptr` you need to make sure your class for which you use PImpl ensures that copying and assigning is done correctly and that it can't happen that you accidentally introduce the possibility that two objects share the same impl object. With `std::unique_ptr` this is given by the type, and you will get a compiler error if you do something wrong. It for sure does not prevent you from all errors, but prevents you form common mistakes. – t.niese Dec 02 '21 at 19:15
  • @t.niese true, did not think of that, usually my classes that have fwd declared members are kind of never copied, but in general you are absolutely right, very nice comment – NoSenseEtAl Dec 02 '21 at 20:00

1 Answers1

6

First, do note that the "atomic reference count" of shared_ptr is only a problem when you actually use it. Since your class is non-copyable (which makes sense for an interface type, and since you were willing to use unique_ptr, it must be non-copyable), the count will only ever be modified on construction and destruction. So... you shouldn't care; even if an atomic reference count was actually a big deal performance-wise, it will be a rounding error next to the cost of the memory allocation/de-allocation that accompanies it.

As to your actual problem, the only reason you're getting a compile error here is because you forced the compiler to instantiate unique_ptr<T>'s destructor from a place where T is still incomplete.

In an actual use of PIMPL, you would have a header file defining the public interface and declaring the private implementation, and also one or more .cpp files providing the implementation of these. unique_ptr<T> can work in those situations so long as the public interface type's destructor is not defined in the header:

///Interface.h
class Impl;

class Interface
{
private:
  unique_ptr<Impl> impl_;

public:
  ~Interface(); //Declaration only; prevent compiler-definition in header
};

///Interface.cpp
class Impl {...};

Interface::~Interface() = default;

This prevents the people including the header from instantiating unique_ptr<Impl>'s destructor.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • This is the correct approach (well certainly one that has worked fine for me in the past). Only comment would be to perhaps nest the `Impl` class privately within `Interface`. This makes it clear that users should not try and create a standalone instance of `Impl`. – DS_London Dec 02 '21 at 19:11