Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Gotchas with template argument deduction for class templates

I was reading the paper regarding template argument deduction for class templates here http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0091r3.html. This feature is in the C++17 standard, and some things had confused me.

template <typename T>
class Something {
public:

    // delete the copy and move constructors for simplicity
    Something(const Something&) = delete;
    Something(Something&&) = delete;

    explicit Something(T&&) { ... }
    explicit Something(const T&) { ... }

    template <typename U, typename EnableIfNotT<U, T>* = nullptr>
    Something(U&&) { ... }
};

Given the above code, if someone tries to instantiate an instance of the above template like this

auto something = Something{std::shared_ptr<int>{}};

will the rvalue reference overload always be called? Since the overload set considered for deduction is

template <typename T>
Something<T> F(T&&) { ... }
template <typename T>
Something<T> F(const T&) { ... }
template <typename T, typename U, typename EnableIfNotT<U, T>*>
Something<T> F(U&&) { ... }
  1. The second overload will never be preferred over the first one (since that is now a forwarding reference overload, instead of being an rvalue reference overload), so what is supposed to happen here?
  2. And it seems like the last one can never be called without explicitly specifying the T parameter, is this the intended behavior?
  3. Also are there any other gotchas or style guidelines that one should keep in mind when using template argument deduction for class templates?
  4. Further are user defined deduction guides required to be after the class definition? For example, can you have the trailing return type in the declaration of the class constructor within the class definition itself? (Unlike the iterator constructor here http://en.cppreference.com/w/cpp/language/class_template_deduction)
like image 451
Curious Avatar asked Oct 29 '22 07:10

Curious


1 Answers

  1. The second overload will never be preferred over the first one (since that is now a forwarding reference overload, instead of being an rvalue reference overload), so what is supposed to happen here?

No, it is not a forwarding reference. This is a key distinction. From [temp.deduct.call]:

A forwarding reference is an rvalue reference to a cv-unqualified template parameter that does not represent a template parameter of a class template (during class template argument deduction ([over.match.class.deduct])).

Your candidates are:

template <typename T>
Something<T> F(T&&);       // this ONLY matches non-const rvalues

template <typename T>
Something<T> F(const T&);  // this matches everything

template <typename T, typename U, typename EnableIfNotT<U, T>*>
Something<T> F(U&&);       // this matches nothing

When you write:

auto something = Something{std::shared_ptr<int>{}};

The T&& constructor is preferred, with T=std::shared_ptr<int>, so you end up with Something<std::shared_ptr<int>> as your class template specialization. If had instead written:

std::shared_ptr<int> p;
auto something = Something{p};

then the T const& constructor is preferred (indeed it is the only viable candidate). Although we end up in the same place: Something<std::shared_ptr<int>>.

  1. And it seems like the last one can never be called without explicitly specifying the T parameter, is this the intended behavior?

Correct, T is a non-deduced context. This makes sense - this constructor exists to do conversions, but you need to specify what you're converting to in order to do the conversion. It would never make sense to have this "just work" for you.

  1. Further are user defined deduction guides required to be after the class definition?

Yes. That's just where they go, by rule. It doesn't make sense to have trailing return type in the constructor - the constructor doesn't "return" anything.

like image 116
Barry Avatar answered Nov 15 '22 07:11

Barry