了解采用自定义删除器的 unique_ptr 的构造函数

Understanding the unique_ptr's constructor which takes a custom deleter

我在说什么

我指的覆盖层是 std::unique_ptr<T,Deleter>::unique_ptr 处的 3 和 4,它们具有以下签名:

unique_ptr( pointer p, /* see below */ d1 ) noexcept;

我的问题

主要是这些:

而且,更详细:

我试图绕过它但没有成功

这里我试着解释一下我的推理。上面预料到的一些问题也散落在文中。

我的理解是在C++17之前,模板类型推导不适用于classes,只适用于函数,所以在创建模板class的实例时,如std::unique_ptr,模板 class 的所有强制性(即没有 = default_type_or_value)模板参数必须通过 <…>.

提供

此外,在/usr/include/c++/10.2.0/bits/unique_ptr.h中,我或多或少看到了这个:

namespace std {
    // …
    template <typename _Tp, typename _Dp = default_delete<_Tp>>
    class unique_ptr {
      public:
        // …
        using deleter_type  = _Dp;
        // …
        template<typename _Del = deleter_type, typename = _Require<is_copy_constructible<_Del>>>
        unique_ptr(pointer __p, const deleter_type& __d) noexcept : _M_t(__p, __d) { }
        // …
    }
    // …
}

其中构造函数在类型参数 _Del 上自行模板化,默认为 class' deleter_type(这是 _Dp 的别名);据此我了解到,如果我错了请纠正我 (*)std::unique_ptr 甚至不能利用 C++ 17 对 classes 的模板类型推导,因此就此重载而言,_Dp 的模板参数仍然是强制性的(即,如果要将删除器对象作为第二个参数传递给构造函数)。

由于是这种情况,我们传递给 std::unique_ptr 的实际类型参数可以用引用声明符装饰,如链接页面中所述。但这就是我迷路的地方,更不用说我确实看到了 _Dp_Del can不同(例如,它们可以通过引用声明符不同),这使我的理解更加复杂。

但是,我将复制解释各种可能情况的页面部分:

3-4) Constructs a std::unique_ptr object which owns p, initializing the stored pointer with p and initializing a deleter D as below (depends upon whether D is a reference type)

  • a) If D is non-reference type A, then the signatures are:

    unique_ptr(pointer p, const A& d) noexcept;
    unique_ptr(pointer p, A&& d) noexcept;
    
  • b) If D is an lvalue-reference type A&, then the signatures are:

    unique_ptr(pointer p, A& d) noexcept;
    unique_ptr(pointer p, A&& d) = delete;
    
  • c) If D is an lvalue-reference type const A&, then the signatures are:

    unique_ptr(pointer p, const A& d) noexcept;
    unique_ptr(pointer p, const A&& d) = delete;
    

In all cases the deleter is initialized from std::forward<decltype(d)>(d). These overloads only participate in overload resolution if std::is_constructible<D, decltype(d)>::value is true.

我只能这样解释引用的文字,有很多疑问。

最后但同样重要的是,链接页面还添加了一些特定于 C++17 的内容:

The program is ill-formed if either of these two constructors is selected by class template argument deduction.

根据我的理解(参见上文 (*)),我一点也不清楚:如何输入这些构造函数会发生推论吗?

所以最重要的问题是:std::unique_ptr<T,Deleter>::unique_ptr 声明的这种复杂性对我作为程序员有用吗?

这些构造函数允许您传入删除器,根据您传入的是左值还是右值,删除器将被复制或移动。

但是,unique_ptr 中的删除器类型 允许 成为删除器的 引用(即使是 D const&).在这种情况下,这些构造函数仍然允许您传入一个左值,然后您的 unique_ptr 将引用该左值。但是,它不会 允许您传入右值。这是因为右值可能会破坏,从而使您的 unique_ptr 带有悬空引用。所以这些构造函数被设置为在编译时捕获这个逻辑错误

如果这个规范不是那么复杂,天真的实现会允许这个逻辑错误(传入右值以绑定到引用删除器)导致 运行-次error 而不是编译时错误。

What does the explanation of /* see below */ actually mean?

它指定了不同类型 D 的构造函数的不同行为,其中 D 是 class 的模板参数,如 std::unique_ptr<T, D>。特别地,它考虑了以下三种情况:

  • D 是一个“普通”值类型,如 std::unique_ptr<int, Deleter>:我们可以传递任何类型 A 的任何对象作为该参数,只要一个 A 可用于 copy/move 构建一个 Deleter 适当。
  • D 是非常量引用类型,如 std::unique_ptr<int, Deleter&>:我们可以提供非常量左值表达式(而 一个非常量const 左值表达式),再次使用可用于构造 Deleter& 的任何类型。 (例如,这可能是派生的 class。)将右值表达式传递给此参数是不允许的,因为存储对(过期的)临时对象的引用没有意义。
  • D是const引用类型,和std::unique_ptr<int, const Deleter&>一样:同上一点,除了const限定的左值表达式也是合法的。

请注意,在所有这些情况下,唯一指针的类型完全由 D 决定:参数中的 A 只允许传递类型 other 的值D 可以用来构建它。

How do I make use of it, as a programmer, when choosing what to pass as a deleter type template argument to std::unique_ptr?

一般情况下,您无需担心。指定 std::unique_ptr<T, D> 以适合您要使用的删除器类型:然后,任何可适当用于构造 D 的合理类型 A 都将起作用,而任何不起作用的类型,不会工作。毕竟这里的详细规范是为了降低用户的复杂度而增加实现的复杂度!

Is the fact that the constructor of std::unique_ptr is templated the reason why the deleter template argument must be provided?

本质上,是的。因果关系走哪条路并不重要。 (它可以被模板化以强制“你不能将这些构造函数与 CTAD 一起使用”,或者它可能必须被模板化,这导致“你不能将这些构造函数与 CTAD 一起使用”:最终这无关紧要。)

If the answer to the preceding quetion is affirmative, then what does the sentence The program is ill-formed if either of these two constructors is selected by class template argument deduction from the linked page mean?

std::unique_ptr foo(value(), deleter()); 是非法的,应该会导致编译错误。这与 CTAD 的工作方式有关,如果您有兴趣,请参阅 cppref's docs on CTAD 以获得更好的想法。

How can _Dp and _Del actually differ, and how is this important?

我们可能会传递类型为 A 的对象,其中 A 是不同于 D 的类型,但所述对象可用于构造类型为 D。此外,我们要转发这种类型:我们不想要不必要的副本。取一个(左值或右值,视情况而定)引用 A 允许我们直接在唯一指针中构造一个 D 。这类似于 .emplace 在标准容器中的用法。

这种复杂性归结为相当简单的使用:

  1. std::unique_ptr<SomeType, SomeDeleter> 有一个构造函数,它的 deleter 参数接受左值或右值。这是有道理的,因为传递给构造函数的删除器将 copied/moved 放入 unique_ptr 对象中。
  2. std::unique_ptr<SomeType, SomeDeleter&> 有一个构造函数,它的 deleter 参数只接受非常量左值。由于 unique_ptr 实例只存储对提供的删除器的引用,因此接受右值是没有意义的(它的生命周期将在 unique_ptr 完成构造后立即结束),并且你'已经声明删除器需要是非常量,因此接受对 const 的引用也没有意义。
  3. std::unique_ptr<SomeType, SomeDeleter const&> 有一个构造函数,它的 deleter 参数接受 const 或非常量左值。不接受右值的原因与 (2) 相同,但在这种情况下,您已声明删除器可以是常量。

例如,如果您取消注释下面任何注释行,则该程序将无法编译。这是理想的,因为所有注释行都会导致危险情况。

struct Deleter
{
    void operator()(int* ptr) const
    {
        delete ptr;
    }
};

int main() {
    Deleter d;
    Deleter const dc;
    std::unique_ptr<int, Deleter> p1{new int{}, d};
    std::unique_ptr<int, Deleter> p2{new int{}, dc};
    std::unique_ptr<int, Deleter> p3{new int{}, Deleter{}};
    
    std::unique_ptr<int, Deleter&> p4{new int{}, d};
    //std::unique_ptr<int, Deleter&> p5{new int{}, dc};
    //std::unique_ptr<int, Deleter&> p6{new int{}, Deleter{}};
    
    std::unique_ptr<int, Deleter const&> p7{new int{}, d};
    std::unique_ptr<int, Deleter const&> p8{new int{}, dc};
    //std::unique_ptr<int, Deleter const&> p9{new int{}, Deleter{}};
}

Live Demo