The overloads I'm referring to are 3 and 4 at std::unique_ptr<T,Deleter>::unique_ptr, which have this signature:
unique_ptr( pointer p, /* see below */ d1 ) noexcept;
Mainly these:
/* see below */ actually mean?std::unique_ptr?But also, more in detail:
std::unique_ptr is templated the reason why the deleter template argument must be provided?_Dp and _Del actually differ, and how is this important?Here I try to explain my reasoning. Some of the question anticipated above are scattered in the text too.
My understanding is that in before C++17, template type deduction does not apply to classes, but only to functions, so when creating an instance of template class, such as std::unique_ptr, all mandatory (i.e. with no = default_type_or_value) template arguments of the template class must be provided via <…>.
Furthermore, in /usr/include/c++/10.2.0/bits/unique_ptr.h, I see more or less this:
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) { }
// …
}
// …
}
where the constructor is templated itself on the type parameter _Del, which is defaulted to the class' deleter_type (which is an alias for _Dp); from this I understand, correct me if I'm wrong (*), that std::unique_ptr cannot even take advantage of C++17's template type deduction for classes, therefore the template argument for _Dp is still compulsory as far as this overloads are concerned (i.e. if a deleter object is to be passed as second argument to the constructor).
Since this is the case, the actual type argument that we pass to std::unique_ptr can be adorned with reference declarators, as explained at the linked page. But this is where I get lost, not to mention that I do see that in general _Dp and _Del can be different (e.g. they can differ by reference declarators), which complicates my understanding even more.
However, I'll copy the bit of the page that explains the various possible scenarios:
3-4) Constructs a
std::unique_ptrobject which ownsp, initializing the stored pointer withpand initializing a deleterDas below (depends upon whetherDis a reference type)
a) If
Dis non-reference typeA, then the signatures are:unique_ptr(pointer p, const A& d) noexcept; unique_ptr(pointer p, A&& d) noexcept;b) If
Dis an lvalue-reference typeA&, then the signatures are:unique_ptr(pointer p, A& d) noexcept; unique_ptr(pointer p, A&& d) = delete;c) If
Dis 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)>::valueistrue.
The only way I can interpret the quoted text is as follows, with a lot of doubts.
d as an argument to the constructor, we must explicity pass a D as the template argument to... what? To the class and/or to its constructor? Is it even possible to pass template arguments to the constructor?D can be of three kinds
A, that means we want to be able to pass both a (possibly const) lvalue or an rvalue as d, so both overloads taking const A& and A&& are defined.const A&, that means we want to be not able to pass an rvalue as d, therefore the overload taking A&& is deleted, as it would bind to rvalues, and the overload A& is used instad of const A&, because the latter would bind to rvalues too.const A&, that means we want to be able to pass d, so the overload taking const A& is the one to pick, whereas the other one taking const A&& is deleted because that parameter type couldn't bind to lvalues, and const A& doesconst A& from binding to rvalues, which would result in a dangling reference being stored in the std::unique_ptr (the reason for this is here).d? 1. binds it to A&& and 3. bind it to const A&, so the former could steal resources and the latter couldn't.Last but not least, the linked page also adds something specific to C++17:
The program is ill-formed if either of these two constructors is selected by class template argument deduction.
which is not clear at all to me, in light of my understanding (see (*) above): how could type deduction happen for these constructors?
So the bottom line question is: how is this complexity in the way std::unique_ptr<T,Deleter>::unique_ptr is declared useful to me as a programmer?
These constructors allow you to pass in a deleter which will be copied or moved depending on whether you pass in an lvalue or rvalue.
However, the deleter type in unique_ptr is allowed to be a reference to a deleter (even a D const&). In this case, these constructors still allow you to pass in an lvalue, which your unique_ptr will then reference. However it will not allow you to pass in an rvalue. This is because the rvalue is likely to destruct, leaving your unique_ptr with a dangling reference. So these constructors are set up to catch this logic error at compile-time.
Had this specification not been so complex, the naive implementation would have allowed this logic error (passing in an rvalue to bind to a reference deleter) to result in a run-time error instead of a compile-time error.
This complexity boils down to fairly simple use:
std::unique_ptr<SomeType, SomeDeleter> has a constructor that accepts either lvalues or rvalues for its deleter parameter. This makes sense, since the deleter passed to the constructor will be copied/moved into the unique_ptr object.std::unique_ptr<SomeType, SomeDeleter&> has a constructor that accepts only non-const lvalues for its deleter parameter. Since the unique_ptr instance is only storing a reference to the provided deleter it wouldn't make sense to accept an rvalue (its lifetime would end as soon as the unique_ptr was finished being constructed), and you've declared that the deleter needs to be non-const, so accepting a reference-to-const also doesn't make sense.std::unique_ptr<SomeType, SomeDeleter const&> has a constructor that accepts const or non-const lvalues for its deleter parameter. The reasoning for not accepting rvalues is the same as for (2), but in this case you've declared that the deleter can be const.For example, if you uncomment any of the commented lines below, this program would fail to compile. This is ideal, since all of the commented lines lead to dangerous situations.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With