Understanding the unique_ptr's constructor which takes a custom deleter

1.2k Views Asked by At

What I'm talking about

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;

My question(s)

Mainly these:

  • What does the explanation of /* see below */ actually mean?
  • 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?

But also, more in detail:

  • Is the fact that the constructor of std::unique_ptr is templated the reason why the deleter template argument must be provided?
  • 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?
  • How can _Dp and _Del actually differ, and how is this important?

My unsuccessful attempt to get my head around it

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_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.

The only way I can interpret the quoted text is as follows, with a lot of doubts.

  • If we want to pass a deleter 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?
  • That D can be of three kinds
    1. If we specify it as 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.
    2. If we specify it as a 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.
    3. If we specify it as a const A&, that means we want to be able to pass both an lvalue or an rvalue as 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 it would treat rvalues not differently than const A& does, as explained in the answers, most importantly, it binds to rvalues preventing the other overload, const 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).
  • However, what is the different usecase for 1. and 3. when an rvalue is passed as 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?

3

There are 3 best solutions below

5
On BEST ANSWER

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

It specifies the different behaviours of constructors for different types D, where D is the class's template argument, as in std::unique_ptr<T, D>. In particular, it considers the following three cases:

  • D is a "normal" value type, like std::unique_ptr<int, Deleter>: we can pass any object of any type A as that parameter, as long as an A can be used to copy/move construct a Deleter as appropriate.
  • D is a non-const reference type, like std::unique_ptr<int, Deleter&>: we can supply a non-const lvalue expression (and only a non-const lvalue expression), again with any type that can be used to construct a Deleter&. (This could be a derived class, for instance.) Passing rvalue expressions to this parameter is disallowed, since it doesn't make sense to store a reference to an (expired) temporary.
  • D is a const reference type, like std::unique_ptr<int, const Deleter&>: same as the above point, except const-qualified lvalue expressions are also legal.

Note that in all these cases, the type of the unique pointer is purely decided by D: the As in the parameter just allow passing values of types other than D that can be used to construct it.

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?

In general, you don't need to worry about it. Specify std::unique_ptr<T, D> as appropriate for the deleter type you want to use: then, any sensible type A that can be appropriately used to construct D will work, and any that wouldn't work, won't work. The detailed specification here is increasing the implementation complexity for the purpose of reducing user complexity, after all!

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

In essence, yes. Which way around the causation goes doesn't matter. (It could be templated to enforce "you may not use these constructors with CTAD", or it may have to be templated which results in "you may not use these constructors with CTAD": ultimately it doesn't matter.)

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?

That std::unique_ptr foo(value(), deleter()); is illegal, and should result in a compilation error. This is related to the way that CTAD works, see cppref's docs on CTAD to get a better idea if you're so interested.

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

We might pass an object of type A, where A is a distinct type from D, but where said object can be used to construct an object of type D. Moreover, we want to forward this type: we do not want unnecessary copies. Taking a (lvalue or rvalue, as appropriate) reference to A allows us to directly construct a D in the unique pointer. This is similar to the usage of .emplace in the standard containers.

6
On

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.

2
On

This complexity boils down to fairly simple use:

  1. 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.
  2. 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.
  3. 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