4

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?

Enlico
  • 23,259
  • 6
  • 48
  • 102
  • I think something to keep in mind is that `std::unique_ptr` tries to have no memory overhead compared to a raw pointer, which it can only do for certain types of destructors. This may be introducing some extra complexity in how they have to define the consturctor. – François Andrieux Dec 08 '20 at 22:45

3 Answers3

3

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.

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
2

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

Miles Budnek
  • 28,216
  • 2
  • 35
  • 52
  • From your example I'd be tempted to say that the `_Del` template parameter of the constructor is what gets deduced based on the second argument you pass in the braces, and whether it is "compatible" with the template argument that you pass in angle brackets determines if the code compiles (and what it means) or not. Is this correct? – Enlico Dec 08 '20 at 23:27
  • 1
    Nothing gets deduced here. The `_Del` template parameter in the libstdc++ implementation is just there to support SFINAE. The constructor parameter itself has the type `deleter_type&` where `deleter_type` is an alias for the type given as `unique_ptr`'s second template parameter. – Miles Budnek Dec 08 '20 at 23:37
1

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.

N. Shead
  • 3,828
  • 1
  • 16
  • 22
  • You write _We might pass a type `A` that isn't `D`_. How can I pass a _type_ to the constructor and a type to the class at the same time? Unless you mean that we can pass to the constructor an _object_ of a type `A` which is deduced to be not the same as the type `D` that we _do_ pass to `std::unique_ptr`. I'm sorry but I'm still a bit confused. – Enlico Dec 08 '20 at 23:34
  • Furthermore, several times you write _we can pass [...]-value ref_. Shouldn't that be _we can pass [...]-value_? My understanding is that template type deduction simply doesn't care/know if the actual argument is an object or a reference to an object. The most it knows is whether the argument is an rvalue or lvalue. – Enlico Dec 08 '20 at 23:39
  • @Enlico Yes, your understanding of what I mean is correct. Remember a constructor is a function; you can make it a template with its own set of template parameters as well. – N. Shead Dec 08 '20 at 23:39
  • @Enlico Maybe that was poor wording on my part. I was meaning that the type of the expression passed to parameter `A&` (post temporary materialisation) is an lvalue reference etc.; value category (lvalue, rvalue, xvalue, etc.) is distinct from the type. In this case it's not the template deduction determining reference-ness, it's the fact that the overloads as specified require a certain kind of reference: passing `x` will bind `x` as an lvalue reference, for instance. – N. Shead Dec 08 '20 at 23:44
  • @Enlico I've clarified my wording a little bit. – N. Shead Dec 08 '20 at 23:55