c++language-lawyerc++20compiler-bugnon-type-template-parameter

Is it possible to determine whether an object is of a class type or not using a trait through a non-type template parameter in C++20?


Consider this code:

#include <type_traits>

template <typename T>
struct wrapper {
    T& ref;
    constexpr wrapper(T& ref) : ref(ref) {}
};

// Trait that checks whether a type is of the form `wrapper<T>`
template <typename T>
struct is_wrapper_type : std::false_type {};

template <typename T>
struct is_wrapper_type<wrapper<T>> : std::true_type {};

// Trait that checks whether an object is of the type `wrapper<T>`
template <auto& Value>
struct is_wrapper_object;

template <auto& Value>
    requires (!is_wrapper_type<std::decay_t<decltype(Value)>>::value)
struct is_wrapper_object<Value> : std::false_type {};

template <auto& Value>
    requires is_wrapper_type<std::decay_t<decltype(Value)>>::value
struct is_wrapper_object<Value> : std::true_type {};

int main() {
    static constexpr int v = 42;
    static_assert(!is_wrapper_object<v>::value);
    static constexpr wrapper w {v};
    static_assert(is_wrapper_object<w>::value);
}

The above fails to compile in Clang with the errors shown below but compiles successfully in GCC.

<source>:30:20: error: implicit instantiation of undefined template 'is_wrapper_object<v>'
   30 |     static_assert(!is_wrapper_object<v>::value);
      |                    ^
<source>:18:8: note: template is declared here
   18 | struct is_wrapper_object;
      |        ^
<source>:32:19: error: implicit instantiation of undefined template 'is_wrapper_object<w>'
   32 |     static_assert(is_wrapper_object<w>::value);
      |                   ^
<source>:18:8: note: template is declared here
   18 | struct is_wrapper_object;
      |        ^

Which compiler is correct here?

I believe GCC should be correct here as the above code should be legal on paper, however, I am not sure whether that is really the case.

Moreover, if I change the definition of the trait is_wrapper_object to this:

template <auto& Value>
struct is_wrapper_object : std::false_type {};

template <auto& Value>
    requires is_wrapper_type<std::decay_t<decltype(Value)>>::value
struct is_wrapper_object<Value> : std::true_type {};

It compiles on Clang as well, albeit with wrong output.


Solution

  • GCC is correct.

    Note that simply replacing your auto& with const auto& makes the code compile for both. Whatever the issue is, it has something to do with placeholder type specifiers in partial specializations.

    Note that:

    If the type T of a template-parameter contains a placeholder type or a placeholder for a deduced class type ([dcl.type.class.deduct]), the type of the parameter is the type deduced for the variable x in the invented declaration

    T x = E ;
    

    - [temp.arg.nontype] p1

    In other words, auto& in a template parameter should be const int& and const wrapper& after such deduction. It should not be necessary for the user to provide const themselves.

    I was unable to find a relevant LLVM bug report, so I have submitted one: LLVM Bug 77189.

    Minimal Reproducible Example

    See https://godbolt.org/z/rhKsWKdPz

    #include <type_traits>
    
    template <auto& Value, int>
    struct test : std::false_type {};
    
    template <auto& Value>
    struct test<Value, 0> : std::true_type {};
    
    int main() {
        static constexpr int v = 42;
        static_assert(test<v, 0>::value); // fails for clang
    }
    

    From this, we can tell that clang is completely unable to deduce Value within the partial specialization of test.