Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Return std::unique_ptr<T> from factory function creating fully hidden implementation of pure virtual interface

I was reading the Smart Pointer Programming Techniques provided in the boost documentation.

In the Section "using abstract classes for implementation hiding", they provide a nice idiom to fully hide an implementation behind pure virtual interface. For example:

// Foo.hpp

#include <memory>

class Foo {
 public:
  virtual void Execute() const = 0;
 protected:
  ~Foo() = default;
};

std::shared_ptr<const Foo> MakeFoo();

and

// Foo.cpp

#include "Foo.hpp"
#include <iostream>

class FooImp final
    : public Foo {
public:
  FooImp()                         = default;
  FooImp(const FooImp&)            = delete;
  FooImp& operator=(const FooImp&) = delete;
  void Execute() const override {
    std::cout << "Foo::Execute()" << std::endl;
  }
};

std::shared_ptr<const Foo> MakeFoo() {
  return std::make_shared<const FooImp>();
}

Regarding the protected, non-virtual destructor in class Foo, the document states:

Note the protected and nonvirtual destructor in the example above. The client code cannot, and does not need to, delete a pointer to X; the shared_ptr<X> instance returned from createX will correctly call ~X_impl.

which I believe I understand.

Now, it seems to me that this nice idiom could be used to produce a singleton-like entity if a factory function returned std::unique_ptr<Foo>; the user would be forced to move the pointer, with a compile-time guarantee that no copies exist.

But, alas, I cannot make the code work unless I change ~Foo() = default from protected to public, and I do not understand why.

In other words, this does not work:

std::unique_ptr<const Foo> MakeUniqueFoo() {
    return std::make_unique<const FooImp>();
}

My questions:

  1. Could you explain me why do I need to make public ~Foo() = default?
  2. Would it be dangerous to just remove protected?
  3. Is the singleton-like idea even worth it?
like image 567
Escualo Avatar asked Dec 25 '22 17:12

Escualo


1 Answers

  1. The issue has to do with how deleters work in smart pointers.

    In shared_ptr, the deleter is dynamic. When you have std::make_shared<const FooImp>();, the deleter in that object will call ~FooImpl() directly:

    The object is destroyed using delete-expression or a custom deleter that is supplied to shared_ptr during construction.

    That deleter will be copied onto the shared_ptr<const Foo> when it's created.

    In unique_ptr, the deleter is part of the type. It's:

    template<
        class T,
        class Deleter = std::default_delete<T>
    > class unique_ptr;
    

    So when you have unique_ptr<const Foo>, that will call ~Foo() directly - which is impossible since ~Foo() is protected. That's why when you make Foo() public, it works. Works, as in, compiles. You would have to make it virtual too - otherwise you'd have undefined behavior by only destructing the Foo part of FooImpl.

  2. It's not dangerous. Unless you forget to make the destructor virtual, which, it bears repeating, will cause undefined behavior.

  3. This isn't really singleton-like. As to whether or not it's worth it? Primarily opinion based.

like image 71
Barry Avatar answered Mar 29 '23 22:03

Barry