了解采用自定义删除器的 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;
我的问题
主要是这些:
/* see below */
的解释到底是什么意思?
- 作为程序员,在选择将什么作为删除器类型模板参数传递给
std::unique_ptr
时,我该如何使用它?
而且,更详细:
std::unique_ptr
的构造函数是模板化的事实是否是必须提供删除模板参数 的原因?
- 如果前面问题的答案是肯定的,那么The program is ill-formed if these both constructors are selected by class template argument deduction 来自链接页面是什么意思?
_Dp
和 _Del
究竟有何不同,这有何重要性?
我试图绕过它但没有成功
这里我试着解释一下我的推理。上面预料到的一些问题也散落在文中。
我的理解是在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
.
我只能这样解释引用的文字,有很多疑问。
- 如果我们想将删除器
d
作为参数传递给构造函数,我们必须显式传递一个D
作为模板参数...什么?到 class
and/or 到它的构造函数?甚至可以将模板参数传递给构造函数吗?
- 那
D
可以分为三种
- 如果我们将其指定为
A
,这意味着我们希望能够将左值(可能是 const
)或右值作为 d
传递,因此两个重载都采用const A&
和 A&&
已定义。
- 如果我们将其指定为
<s>const</s> A&
,这意味着我们希望成为无法 将右值作为d
传递,因此删除采用A&&
的重载,因为它会绑定到右值,而重载A&
用于代替 const A&
,因为后者也会绑定到右值。
- 如果我们将其指定为
const A&
,这意味着我们希望能够传递两个一个左值或一个右值[=161] =] 为 d
,所以选择 const A&
的重载,而另一个选择 const A&&
的重载是 delete
d,因为该参数类型无法绑定到左值,并且 它对待右值的方式与 const A&
对待右值没有区别 , 如答案中所述,最重要的是,它绑定到右值以防止其他过载, const A&
从绑定到右值,这将导致悬空引用存储在 std::unique_ptr
中(原因是 here).
- 但是,当右值作为
d
传递时,1. 和 3. 的不同用例是什么? 1. 绑定到A&&
3. 绑定到const A&
,所以前者可以窃取资源,后者不能。
最后但同样重要的是,链接页面还添加了一些特定于 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
在标准容器中的用法。
这种复杂性归结为相当简单的使用:
std::unique_ptr<SomeType, SomeDeleter>
有一个构造函数,它的 deleter 参数接受左值或右值。这是有道理的,因为传递给构造函数的删除器将 copied/moved 放入 unique_ptr
对象中。
std::unique_ptr<SomeType, SomeDeleter&>
有一个构造函数,它的 deleter 参数只接受非常量左值。由于 unique_ptr
实例只存储对提供的删除器的引用,因此接受右值是没有意义的(它的生命周期将在 unique_ptr
完成构造后立即结束),并且你'已经声明删除器需要是非常量,因此接受对 const 的引用也没有意义。
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{}};
}
我在说什么
我指的覆盖层是 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 these both constructors are selected by class template argument deduction 来自链接页面是什么意思?
_Dp
和_Del
究竟有何不同,这有何重要性?
我试图绕过它但没有成功
这里我试着解释一下我的推理。上面预料到的一些问题也散落在文中。
我的理解是在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 ownsp
, initializing the stored pointer withp
and initializing a deleterD
as below (depends upon whetherD
is a reference type)
a) If
D
is non-reference typeA
, 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 typeA&
, 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 typeconst 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 ifstd::is_constructible<D, decltype(d)>::value
istrue
.
我只能这样解释引用的文字,有很多疑问。
- 如果我们想将删除器
d
作为参数传递给构造函数,我们必须显式传递一个D
作为模板参数...什么?到class
and/or 到它的构造函数?甚至可以将模板参数传递给构造函数吗? - 那
D
可以分为三种- 如果我们将其指定为
A
,这意味着我们希望能够将左值(可能是const
)或右值作为d
传递,因此两个重载都采用const A&
和A&&
已定义。 - 如果我们将其指定为
<s>const</s> A&
,这意味着我们希望成为无法 将右值作为d
传递,因此删除采用A&&
的重载,因为它会绑定到右值,而重载A&
用于代替const A&
,因为后者也会绑定到右值。 - 如果我们将其指定为
const A&
,这意味着我们希望能够传递两个一个左值或一个右值[=161] =] 为d
,所以选择const A&
的重载,而另一个选择const A&&
的重载是delete
d,因为该参数类型无法绑定到左值,并且它对待右值的方式与, 如答案中所述,最重要的是,它绑定到右值以防止其他过载,const A&
对待右值没有区别const A&
从绑定到右值,这将导致悬空引用存储在std::unique_ptr
中(原因是 here).
- 如果我们将其指定为
- 但是,当右值作为
d
传递时,1. 和 3. 的不同用例是什么? 1. 绑定到A&&
3. 绑定到const A&
,所以前者可以窃取资源,后者不能。
最后但同样重要的是,链接页面还添加了一些特定于 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
在标准容器中的用法。
这种复杂性归结为相当简单的使用:
std::unique_ptr<SomeType, SomeDeleter>
有一个构造函数,它的 deleter 参数接受左值或右值。这是有道理的,因为传递给构造函数的删除器将 copied/moved 放入unique_ptr
对象中。std::unique_ptr<SomeType, SomeDeleter&>
有一个构造函数,它的 deleter 参数只接受非常量左值。由于unique_ptr
实例只存储对提供的删除器的引用,因此接受右值是没有意义的(它的生命周期将在unique_ptr
完成构造后立即结束),并且你'已经声明删除器需要是非常量,因此接受对 const 的引用也没有意义。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{}};
}