Search code examples
c++templatescompiler-errorslanguage-lawyertemplate-argument-deduction

Compiler variance in function template argument deduction


The following program:

#include <type_traits>

template<typename T, bool b>
struct S{
    S() = default;
    
    template<bool sfinae = true,
             typename = std::enable_if_t<sfinae && !std::is_const<T>::value>>
    operator S<T const, b>() { return S<T const, b>{}; }
};

template<typename T, bool b1, bool b2>
void f(S<const std::type_identity_t<T>, b1>,
                                 // ^- T in non-deduced context for func-param #1 
       S<T, b2>)
      // ^- T deduced from here
{}                         

int main() {
    S<int, true> s1{};
    S<int, false> s2{};
    f(s1, s2);
}

is accepted by GCC (11.2) but rejected by Clang (13) and MSVC (19.latest), all for -std=c++20 / /std:c++20 (DEMO).

  • What compiler is correct here?

Solution

  • This is governed by [temp.deduct.call], particularly /4:

    In general, the deduction process attempts to find template argument values that will make the deduced A identical to A (after the type A is transformed as described above). However, there are three cases that allow a difference: [...]

    In the OP's example, A is S<const int, true> and the (transformed) A is S<int, int>, and none of these [three] cases applies here, meaning deduction fails.

    We may experiment with this by tweaking the program such that the deduced A vs tranformed A difference falls under one of the three cases; say [temp.deduct.call]/4.3

    • If P is a class and P has the form simple-template-id, then the transformed A can be a derived class D of the deduced A. [...]
    #include <type_traits>
    
    template<typename T, bool b>
    struct S : S<const T, b> {};
    
    template<typename T, bool b>
    struct S<const T, b> {};
    
    template<typename T, bool b1, bool b2>
    void f(S<const std::type_identity_t<T>, b1>, S<T, b2>){}                         
    
    int main() {
        S<int, true> s1{};
        S<int, true> s2{};
        f(s1, s2);
    }
    

    This program is correctly accepted by all three compilers (DEMO).

    Thus, GCC most likely has a bug here, as the error argued for above is diagnosable (not ill-formed NDR). As I could not find an open bug for the issues, I've filed:

    • Bug 103333 - [accepts-invalid] function template argument deduction for incompatible 'transformed A' / 'deduced A' pair

    We may also note that [temp.arg.explicit]/7 contains a special case where implicit conversions are allowed to convert an argument type to the function parameter type:

    Implicit conversions ([conv]) will be performed on a function argument to convert it to the type of the corresponding function parameter if the parameter type contains no template-parameters that participate in template argument deduction.

    This does not apply in OP's example, though, as the (function) parameter type S<const std::type_identity_t<T>, b1> contains also the (non-type) template parameter b1, which participates in template argument deduction.

    However in the following program:

    #include <type_traits>
    
    template<typename T>
    struct S{
        S() = default;
        
        template<bool sfinae = true,
                 typename = std::enable_if_t<sfinae && !std::is_const<T>::value>>
        operator S<T const>() { return S<T const>{}; }
    };
    
    template<typename T>
    void f(S<const std::type_identity_t<T>>, S<T>) {}                         
    
    int main() {
        S<int> s1{};
        S<int> s2{};
        f(s1, s2);
    }
    

    the (function) parameter type is A<std::type_identity_t<T>>, and the only template parameter in it is T which does not participate in template argument deduction for that parameter-argument pair (P/A). Thus, [temp.arg.explicit]/7 do apply here and the program is well-formed.