【问题标题】:Understanding the unique_ptr's constructor which takes a custom deleter了解 unique_ptr 的构造函数,它采用自定义删除器
【发布时间】:2020-12-08 22:21:27
【问题描述】:

我在说什么

我指的是 std::unique_ptr<T,Deleter>::unique_ptr 的 3 和 4,它们有这个签名:

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

我的问题

主要是这些:

  • /* see below */的解释究竟是什么意思?
  • 作为程序员,在选择将什么作为删除器类型模板参数传递给std::unique_ptr 时,我该如何使用它?

而且,更详细:

  • std::unique_ptr 的构造函数是模板化的事实是否是必须提供删除器模板参数的原因?
  • 如果上一个问题的答案是肯定的,那么句子The program is ill-formed if this two constructors are selected by class template argument deduction是什么意思?
  • _Dp 和 _Del 究竟有何不同,这有多重要?

我试图绕过它的失败尝试

在这里我试图解释我的推理。上面预期的一些问题也散布在文本中。

我的理解是,在C++17之前,模板类型推导不适用于类,只适用于函数,所以在创建模板类的实例时,比如std::unique_ptr,都是强制的(即没有@ 987654331@) 模板类的模板参数必须通过<…>提供。

此外,在/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上被模板化,默认为类'deleter_type(这是_Dp的别名);据我了解,如果我错了,请纠正我 (*),std::unique_ptr 甚至无法利用 C++17 的类模板类型推导,因此就重载而言,_Dp 的模板参数仍然是强制性的(即,如果要将删除器对象作为第二个参数传递给构造函数)。

既然是这种情况,我们传递给std::unique_ptr 的实际类型参数可以用引用声明符进行修饰,如链接页面中所述。但这就是我迷路的地方,更不用说我确实看到一般_Dp 和_Del 可以有所不同(例如,它们可能因引用声明符而异),这使我的理解更加复杂。

不过,我将复制页面中解释各种可能情况的部分内容:

3-4) 构造一个拥有p的std::unique_ptr对象,用p初始化存储的指针并初始化一个删除器D如下(取决于D是否为引用类型)

  • a) 如果D 是非引用类型A,则签名为:

    unique_ptr(pointer p, const A& d) noexcept;
    unique_ptr(pointer p, A&& d) noexcept;
    
  • b) 如果D 是左值引用类型A&amp;,则签名为:

    unique_ptr(pointer p, A& d) noexcept;
    unique_ptr(pointer p, A&& d) = delete;
    
  • c) 如果D 是一个左值引用类型const A&amp;,那么签名是:

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

在所有情况下,删除器都是从std::forward&lt;decltype(d)&gt;(d) 初始化的。这些重载仅在 std::is_constructible&lt;D, decltype(d)&gt;::value 为 true 时参与重载解析。

我能理解引用的文字的唯一方法如下,有很多疑问。

  • 如果我们想将删除器 d 作为参数传递给构造函数,我们必须明确地将 D 作为模板参数传递给...什么?到class 和/或它的构造函数?甚至可以将模板参数传递给构造函数吗?
  • 那个D可以是三种
    1. 如果我们将其指定为A,这意味着我们希望能够将(可能是const)左值或右值作为d 传递,因此采用const A&amp; 和A&amp;&amp; 的重载都是已定义。
    2. 如果我们将其指定为<s>const</s> A&amp;,这意味着我们希望无法将右值作为d 传递,因此重载采用@987654371 @ 被删除,因为它会绑定到右值,而重载 A&amp; 用于代替const A&amp;,因为后者也会绑定到右值。
    3. 如果我们将其指定为const A&amp;,这意味着我们希望能够将both 一个左值 或一个右值 作为d 传递,所以重载采用const A&amp; 是要选择的,而采用const A&amp;&amp; 的另一个是deleted,因为该参数类型无法绑定到左值,并且它对待右值的处理方式与const A&amp; 没有区别, 正如答案中所解释的,最重要的是,它绑定到右值,防止其他重载 const A&amp; 绑定到右值,这将导致悬空引用存储在 std::unique_ptr 中(原因因为这是here)。
  • 但是,当右值作为d 传递时,1. 和 3. 的不同用例是什么? 1.绑定A&amp;&amp;和3.绑定const A&amp;,前者可以盗取资源,后者不能。

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

如果通过类模板参数推导选择这两个构造函数中的任何一个,则程序是非良构的。

根据我的理解(参见上面的(*)),我根本不清楚:这些构造函数如何进行类型推断? p>

所以底线问题是:这种复杂性在声明std::unique_ptr&lt;T,Deleter&gt;::unique_ptr 的方式对我作为程序员有用吗?

【问题讨论】:

  • 我认为需要记住的是,std::unique_ptr 与原始指针相比尝试没有内存开销,它只能用于某些类型的析构函数。这可能会在他们必须如何定义构造函数时引入一些额外的复杂性。

标签: c++ templates c++17 unique-ptr type-deduction


【解决方案1】:

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

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

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

【讨论】:

  • 引用 (D const&amp;) 是否会在需要时保持右值存活?
  • 不。成员 const&amp; 不延长构造函数参数的生命周期。
  • this 的解释正确吗?
  • 我认为是这样。诚然,我不是语言律师。
  • @HowardHinnant • 这让我笑出声来。 :-)
【解决方案2】:

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

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

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

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

【讨论】:

  • 从您的示例中,我很想说构造函数的 _Del 模板参数是根据您在大括号中传递的第二个参数以及它是否“兼容”而得出的使用您在尖括号中传递的模板参数确定代码是否编译(及其含义)。这是正确的吗?
  • 这里没有推论。 libstdc++ 实现中的_Del 模板参数只是为了支持SFINAE。构造函数参数本身的类型为deleter_type&amp;,其中deleter_type 是作为unique_ptr 的第二个模板参数给出的类型的别名。
【解决方案3】:

/* see below */的解释究竟是什么意思?

它指定不同类型D的构造函数的不同行为,其中D是类的模板参数,如std::unique_ptr&lt;T, D&gt;。具体来说,它考虑了以下三种情况:

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

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

作为程序员,在选择将什么作为删除器类型模板参数传递给std::unique_ptr 时,我该如何使用它?

一般来说,您无需担心。根据您要使用的删除器类型指定std::unique_ptr&lt;T, D&gt;:然后,任何可用于构造D 的合理类型A 都将起作用,而任何不起作用的类型都将不起作用。这里的详细规范,毕竟是为了降低用户复杂度而增加了实现复杂度!

std::unique_ptr 的构造函数是模板化的事实是必须提供删除器模板参数的原因吗?

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

如果上一个问题的答案是肯定的,那么句子The program is ill-formed if this two constructors are selected by class template argument deduction是什么意思?

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

_Dp 和 _Del 究竟有何不同,这有多重要?

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

【讨论】:

  • 你写我们可能会传递一个不是D的类型A。如何同时将 type 传递给构造函数和类型?除非您的意思是我们可以将 A 类型的 object 传递给构造函数,这被推断为与我们 do 传递的 D 类型不同到std::unique_ptr。对不起,我还是有点困惑。
  • 另外,你写几次我们可以传递[...]-value ref。那不应该是我们可以传递[...]-value吗?我的理解是模板类型推导根本不关心/知道实际参数是对象还是对对象的引用。它知道的最多的是参数是右值还是左值。
  • @Enlico 是的,您对我的意思的理解是正确的。记住构造函数是一个函数;您也可以将其设为具有自己的模板参数集的模板。
  • @Enlico 也许我的措辞很糟糕。我的意思是传递给参数A&amp;(临时实现后)的表达式类型是左值引用等;值类别(左值、右值、xvalue 等)与类型不同。在这种情况下,确定引用性的不是模板推导,而是指定的重载需要某种引用的事实:例如,传递x 会将x 绑定为左值引用。
  • @Enlico 我已经澄清了我的措辞。
猜你喜欢
  • 2015-08-15
  • 2020-02-25
  • 1970-01-01
  • 2013-03-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-06-16
相关资源
最近更新 更多