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



我在说什么

我所指的重载是std::unique_ptr<T,Deleter>::unique_ptr的3和4,它们具有以下签名:

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

我的问题

主要有:

  • /* see below */的解释实际上意味着什么
  • 作为一名程序员,在选择将什么作为deleter类型的模板参数传递给std::unique_ptr时,我如何利用它

但也有更详细的信息:

  • std::unique_ptr的构造函数是模板化的,这是必须提供deleter模板参数的原因吗
  • 如果前面的问题的答案是肯定的,那么如果这两个构造函数中的任何一个是通过类模板参数推导从链接页中选择的,那么这句话是什么意思
  • _Dp_Del实际上有什么不同,这又有什么重要意义

我试图绕过它但没有成功

在这里我试着解释我的推理。上文预期的一些问题也分散在案文中。

我的理解是,在C++17之前,模板类型推导不适用于类,而仅适用于函数,因此在创建模板类的实例时,例如std::unique_ptr,模板类的所有强制性(即没有= 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上模板化的,默认为类'deleter_type(它是_Dp的别名);根据这一点,如果我错了(*),请纠正我,std::unique_ptr甚至不能利用C++17的类模板类型推导,因此就这种重载而言,_Dp的模板参数仍然是强制性的(即,如果deleter对象要作为第二个参数传递给构造函数)。

由于是这种情况,我们传递给std::unique_ptr的实际类型参数可以用引用声明符进行修饰,如链接页所述。但这正是我迷失的地方,更不用说我确实看到_Dp_Del通常可以不同(例如,它们可以因引用声明符而不同),这使我的理解更加复杂。

然而,我将复制页面中解释各种可能场景的部分:

3-4)构造一个拥有pstd::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&,则签名为:

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

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

在所有情况下,删除程序都是从std::forward<decltype(d)>(d)初始化的。如果std::is_constructible<D, decltype(d)>::valuetrue,则这些过载仅参与过载解决。

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

  • 如果我们想将deleterd作为参数传递给构造函数,我们必须显式地将D作为模板参数传递给…什么?至class和/或其构造函数?是否可以将模板参数传递给构造函数
  • D可以有三种
    1. 如果我们将其指定为A,这意味着我们希望能够传递一个(可能是const)左值或一个右值作为d,因此定义了取const A&A&&的两个重载
    2. 如果我们将其指定为constA&,这意味着我们希望不能将右值传递为d,因此删除采用A&&的过载,因为它将绑定到右值,并且使用过载A&代替const A&,因为后者也将绑定到右值
    3. 如果我们将其指定为const A&,这意味着我们希望能够将传递一个左值或一个右值作为d,因此采用const A&的重载是要选择的重载,而采用const A&&的另一个重载是deleted,因为该参数类型无法绑定到左值,并且它对待右值的方式与const A&处理右值的方法没有什么不同,如答案中所述,最重要的是,它与右值结合,防止另一个过载const A&与右值绑定,这将导致挂起的引用存储在std::unique_ptr中(原因在这里)
  • 然而,1的不同用例是什么。和3。当一个右值作为d传递时?1.将其与CCD_ 56和3。将其绑定到const A&,因此前者可以窃取资源,而后者不能

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

如果通过类模板参数推导选择了这两个构造函数中的任何一个,则程序格式错误。

根据我的理解,我一点也不清楚(请参见上面的(*)):这些构造函数如何进行类型推导?

因此,最重要的问题是:作为一名程序员,std::unique_ptr<T,Deleter>::unique_ptr的这种复杂性是如何对我有用的

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

但是,unique_ptr中的deleter类型是允许作为deleter(甚至是D const&)的引用。在这种情况下,这些构造函数仍然允许您传入一个左值,然后unique_ptr将引用该左值。然而,它将不允许传入右值。这是因为右值可能会被破坏,留下一个挂起的引用作为unique_ptr。因此,这些构造函数被设置为在编译时捕获此逻辑错误

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

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

  1. std::unique_ptr<SomeType, SomeDeleter>有一个构造函数,它接受其deleter参数的左值或右值。这是有意义的,因为传递给构造函数的deleter将被复制/移动到unique_ptr对象中
  2. std::unique_ptr<SomeType, SomeDeleter&>有一个构造函数,它的deleter参数只接受非常值。由于unique_ptr实例只存储对所提供的deleter的引用,因此接受右值是没有意义的(一旦构造完unique_ptr,它的生存期就会结束),并且您已经声明了deleter需要是非const,因此接受对const的引用也是没有意义的
  3. std::unique_ptr<SomeType, SomeDeleter const&>有一个构造函数,它接受其deleter参数的常量或非常量值。不接受右值的理由与(2)相同,但在本例中,您已经声明了deleter可以是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{}};
}

实时演示

/* see below */的解释实际上意味着什么?

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

  • CCD_;正常的";值类型,如std::unique_ptr<int, Deleter>:我们可以传递任何类型的任何对象A作为该参数,只要A可以用于适当地复制/移动构造Deleter即可
  • D是一种非常数引用类型,与std::unique_ptr<int, Deleter&>类似:我们可以提供一个非常数左值表达式(是一个非常量左值表达式),也可以提供任何可用于构造Deleter&的类型。(例如,这可能是派生类。)不允许向该参数传递右值表达式,因为存储对(过期的)临时的引用是没有意义的
  • D是一个const引用类型,与std::unique_ptr<int, const Deleter&>类似:与上面的点相同,只是const限定的左值表达式也是合法的

注意,在所有这些情况下,唯一指针的类型完全由D决定:参数中的As只允许传递除D之外的类型的值,这些值可用于构造它。

作为一名程序员,在选择将什么作为deleter类型的模板参数传递给std::unique_ptr时,我如何利用它?

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

std::unique_ptr的构造函数是模板化的,这是必须提供deleter模板参数的原因吗?

本质上,是的。因果关系朝哪个方向发展并不重要。(它可以被模板化以强制执行"您可能不将这些构造函数与CTAD一起使用",或者它可能必须被模板化,这导致"您可能不会将这些构造函数用于CTAD":最终这无关紧要。)

如果前面的问题的答案是肯定的,那么如果这两个构造函数中的任何一个是通过类模板参数推导从链接页中选择的,那么这个句子是什么?

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

_Dp和_Del实际上有什么不同,这有多重要?

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

相关内容

  • 没有找到相关文章

最新更新