Search code examples
c++language-lawyerc++20ctad

CTAD rules regarding implicit deduction guides


I would expect the construction of b as in b{d{}} to result in a compilation error, as b's templated constructor is not viable due to its constraints. However, GCC and MSVC seem to accept this code and deduce b to be b<int> instead of b<d>. Which seems odd, as I don't see how b's implicit deduction guide would be much different from template<typename T> b(T) -> b<T>;.

Which compiler can we trust this time? What does the C++20 standard have to say about this behavior?

template<typename T>
struct b {
    b() = default;
    b(T) requires false {}
};
struct d : b<int> {};
inline constexpr auto z = b{d{}};

Demo


Clang's error message:

<source>:7:27: error: no matching constructor for initialization of 'b<d>'
    7 | inline constexpr auto z = b{d{}};
      |                           ^~~~~~
<source>:2:8: note: candidate constructor (the implicit copy constructor)
not viable: no known conversion from 'd' to 'const b<d>' for 1st argument
    2 | struct b {
      |        ^
<source>:2:8: note: candidate constructor (the implicit move constructor)
not viable: no known conversion from 'd' to 'b<d>' for 1st argument
    2 | struct b {
      |        ^
<source>:4:5: note: candidate constructor not viable: constraints not satisfied
    4 |     b(T) requires false {}
      |     ^
<source>:4:19: note: because 'false' evaluated to false
    4 |     b(T) requires false {}
      |                   ^
<source>:3:5: note: candidate constructor not viable: requires 0 arguments,
but 1 was provided
    3 |     b() = default;
      |     ^

Solution

  • We have 5 implicit deduction guides for class template argument deduction in this case. The first two are generated from declared constructors:

    • template <typename T> b() -> b<T>; (can never be used, since T is not deducible)
    • template <typename T> b(T) requires false -> b<T>; (associated constraints never satisfied)

    Then there are two generated from the implicit copy constructor and implicit move constructor:

    • template <typename T> b(const b<T>&) -> b<T>;
    • template <typename T> b(b<T>&&) -> b<T>;

    Finally, during class template argument deduction, there is always an implicit copy deduction candidate as described in [over.match.class.deduct]/1.3:

    An additional function template derived as above from a hypothetical constructor C(C), called the copy deduction candidate.

    This implicit deduction guide, which behaves like template <typename T> b(b<T>) -> b<T>, is separate from the one generated from the actual copy constructor.

    In the case of b{d{}}, deduction succeeds for the last three deduction guides, with T being deduced as the type such that b<T> is a base class of D, namely, int. The implicit deduction guide from the move constructor wins over the one from the copy constructor (because the argument is an rvalue) but the copy deduction candidate wins over both of them thanks to the tiebreaker rule in [over.match.best.general]/2.12:

    ... a viable function F1 is defined to be a better function than another viable function F2 if...

    • [...]
    • F1 is the copy deduction candidate and F2 is not, ...
    • [...]

    CTAD succeeds and deduces b<int>. A second overload resolution is then done to perform the actual initialization b<int>{d{}}. For the actual initialization, the copy deduction candidate doesn't exist. The move constructor is called.

    Clang presumably just has some bug here.