从工厂函数返回 std::unique_ptr<T> 创建纯虚拟接口的完全隐藏实现

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

本文关键字:接口 虚拟 创建 实现 隐藏 gt unique 函数 std ptr lt      更新时间:2023-10-16

我正在阅读boost文档中提供的智能指针编程技术

在">使用抽象类隐藏实现"一节中,它们提供了一个很好的习惯用法,可以将实现完全隐藏在纯虚拟接口后面。例如:

// Foo.hpp
#include <memory>
class Foo {
 public:
  virtual void Execute() const = 0;
 protected:
  ~Foo() = default;
};
std::shared_ptr<const Foo> MakeFoo();

// 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>();
}

关于class Foo中受保护的非虚拟析构函数,文档指出:

注意上面例子中的受保护和非虚拟析构函数。这个客户端代码不能也不需要删除指向X的指针;这个createX返回的shared_ptr<X>实例将正确调用~X_impl

我相信我理解。

现在,在我看来,如果一个工厂函数返回std::unique_ptr<Foo>,那么这个不错的习惯用法可以用来生成一个类似单例的实体;用户将被迫move指针,并在编译时保证不存在副本。

但是,唉,除非我将~Foo() = defaultprotected更改为public,否则我无法使代码正常工作,我不明白为什么。

换句话说,这不起作用:

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

我的问题:

  1. 你能解释一下为什么我需要制作public ~Foo() = default
  2. 仅仅移除protected会有危险吗
  3. 单身汉式的想法值得吗
  1. 这个问题与删除程序如何在智能指针中工作有关。

    shared_ptr中,删除器是动态的。当您有std::make_shared<const FooImp>();时,该对象中的deleter将直接调用~FooImpl()

    使用delete表达式或在构造期间提供给shared_ptr的自定义deleter来销毁对象。

    创建该deleter时,它将被复制到shared_ptr<const Foo>上。

    unique_ptr中,deleter是类型的部分。它是:

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

    所以当你有unique_ptr<const Foo>时,它会直接调用~Foo()——这是不可能的,因为~Foo()就是protected。这就是为什么当你公开Foo()时,它是有效的。Works,如中,编译。您也必须将其设为virtual,否则您只会破坏FooImplFoo部分,从而产生未定义的行为。

  2. 这并不危险。除非你忘记了将析构函数设为虚拟的,否则它将导致未定义的行为。

  3. 这并不是真正的单身汉。至于这是否值得?主要基于意见。

每个shared_ptr存储4个东西:指针、强引用计数、弱引用计数和deleter。

deleter获取构造shared_ptr的类型,并删除类型的,而不是暴露的类型。如果将其强制转换为基shared_ptr,则派生的deleter仍将存储。

默认情况下,unique_ptr不存储这样一个有状态的deleter。

这背后的设计原因是shared_ptr已经在管理额外的资源:考虑到您已经在管理引用计数,添加deleter是很便宜的。

对于unique_ptr,如果没有有状态的deleter,它的开销基本上与原始指针相同。默认情况下添加一个有状态的deleter会使unique_ptr的开销显著增加。

虽然它们都是智能指针,但unique_ptr实际上是最小的,而shared_ptr则要复杂得多。

您可以通过在unique_ptr中添加一个有状态的deleter来解决这个问题。

struct stateful_delete {
  void const* ptr = nullptr;
  void(*f)(void const*) = nullptr;
  template<class T>
  stateful_delete(T const* t):
    ptr(t),
    f([](void const* ptr){
      delete static_cast<T const*>(ptr);
    })
  {}
  template<class T>
  void operator()(T*)const{
    if (f) f(ptr);
  }
};
template<class T>
using unique_ptr_2 = std::unique_ptr<T, stateful_delete>;
template<class T>
unique_ptr_2<T> unique_wrap_2(T* t) {
  return {t, t};
}
template<class T, class...Args>
unique_ptr_2<T> make_unique_2(Args&&...args) {
  return unique_wrap( new T(std::forward<Args>(args)...) );
}

这样的CCD_ 37是CCD_。它们不进行额外的分配(与shared_ptr不同(。它们将使用非虚拟受保护的~Foo和公共~FooImpl

如果我们使用make_shared技术进行统一分配,并在堆上存储等效的ptrf,则可以将unique_ptr_2的大小减少到2个指针。我不确定这种复杂性是否值得节省。

根据Barry的回答,公开它的另一种选择是定义自己的deleter,它可以访问类的~Foo()方法。

示例(使用VS2013尝试(:

template <typename T>
class deleter
{
public:
    void operator()(T* a)
    {
        // Explicitly call the destructor on a.
        a->~A();
    }
};
class A {
    friend class deleter<A>; // Grant access to the deleter.
protected:
    ~A() {
        // Destructor.
    }
};
std::unique_ptr<A, deleter<A>> MakeA()
{
    return std::unique_ptr<A, deleter<A>>(new A());
}