19

I am looking into class template deduction available since C++17. Here are the code I would like to ask about:

#include <iostream>
#include <cmath>
using std::endl;
using std::cout;

template<typename T>
struct MyAbs {
     template<typename U>
     MyAbs(U&& u) : t(std::forward<T>(u))
     { cout << "template" << endl;}

#ifdef ON
    MyAbs(const T& t) : t(t) {}
#endif

    T operator()() const
    {
        return std::abs(t);
    }
    T t;
};

/*
  // may need the following
template<typename U>
MyAbs(U&&) -> MyAbs<typename std::remove_reference<U>::type>;
*/

int main()
{
    const double d = 3.14;
    cout << MyAbs(4.7)() << endl;
    cout << MyAbs(d)() << endl;    
    return 0;
}

When MyAbs(const T&) is not conditionally-compiled (i.e. no -DON), both clang++ and g++ fail to deduce the template parameter, T. Given -DON=1, both compilers build the simple example above.

Firstly, I also guessed that the deduction should fail; the compiler could deduce U but not T. The compile errors I got were what I expected. Please, let me know if I am mistaken.

If I was correct on it, then, I cannot understand why the deduction with U&& succeeds when MyAbs(const T&) is added. What I expected was deducing with U&& fails, and SFINAE allows me to invoke MyAbs(const T&) instead for both cases: 4.7 and d. However, what happened is different. This program seems to invoke the template version for 4.7 and non-template version for d.

$ g++ -Wall -std=c++17 ~/a.cc -DON=1
$ ./a.out 
template
4.7
3.14

It seems that the template version has suddenly become viable. Is this expected? If so, what's the reason?

Barry
  • 286,269
  • 29
  • 621
  • 977
Stephen
  • 609
  • 6
  • 12

1 Answers1

21

Class template argument deduction happens in two stages.

  1. Deduce the class template arguments.
  2. Then, do the actual construction with the concrete class type.

Step 1 only helps you figure out the class template arguments. It does not do anything with regards to the actual constructor (or class template specialization) that may be used in step 2.

The existence of the constructors might drive the deduction, but if a given constructor is used to deduce that says nothing about whether or not it's used to construct.


So, when you just have:

template<typename T>
struct MyAbs {
     template<typename U>
     MyAbs(U&& u);
};

Class template argument deduction fails - you have no deduction guide in your constructor, T is a non-deduced context. The compiler just can't figure out what T you want in MyAbs(4.7) or MyAbs(d).

When you added this one:

template<typename T>
struct MyAbs {
     template<typename U>
     MyAbs(U&& u);

     MyAbs(T const&);
};

Now it can! In both cases, it deduces T as double. And once it does that, then we go ahead and perform overload resolution as if we had typed MyAbs<double> to begin with.

And here, MyAbs<double>(4.7) happens to prefer the forwarding reference constructor (less cv-qualified reference) while MyAbs<double>(d) happens to prefer the other one (non-template preferred to template). And that's okay and expected, just because we used one constructor for deduction doesn't mean we have to use specifically that constructor for construction.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • But when not using U as a separate template type, like so: `MyAbs(T&& t);` and having that single constructor, it doesn't compile for me. I know how to fix it, but I don't understand why I need to. – Miguel Jun 26 '18 at 18:35
  • @Miguel I don't understand the question – Barry Jun 26 '18 at 19:08
  • @Barry How do I get the code works without adding `MyAbs(const T& t)? I.e., Is it possible to enable forwarding reference on the template argument `T` while making the template deduction also work? – wk_j Apr 13 '22 at 06:41
  • 1
    @wk_j You're looking for deduction guides. – Barry Apr 13 '22 at 14:54