admin管理员组

文章数量:1321043

I found a strange behavior with a custom type trait I was writing and further found that different compilers are having different behavior with the trait. The goal is simple, I want to detect if a given function MyFunction is defined/overloaded for a specific input type. I made a trait HasMyFunction to do this. The example code and godbolt link for 3 compilers behavior is below.

To me the clang and gcc behavior is surprising/incorrect. The trait returns false for primitive types and even std::string unless MyFunction for those types is defined PRIOR to the template definition for the trait. However if I use a locally defined/custom type the same trait always returns true, no matter if the class and its MyFunction is defined before or after the trait.

MSVC returns true in all cases regardless of the ordering.

My understanding is that templates don't "exist" until they are instantiated and given that the instantiation in all cases is well after all these other declarations I don't see why this ordering would matter.

What is the correct behavior here, and which of the compilers is non compliant? This does not seem like the type of thing that would be compiler dependent or left to the implementers....

Godbolt

#include <iostream>
#include <type_traits>
#include <string>

/// Helper to determine if a type has a specialization of the MyFunction function
template <typename T, typename = void>
struct HasMyFunction : public std::false_type{};

template <typename T>
struct HasMyFunction<T, std::void_t<decltype(MyFunction(std::declval<T>()))>> : public std::true_type{};


//Trait works no matter the location of this def
struct Test{};
std::string MyFunction(Test)
{
    return "";
}

struct TestBad{};

//Move me up above line 5 and the trait works as you would expect (true)
//MSVC seems to work in all cases
//gcc/clang gives false unless this is declared before the template....
std::string MyFunction(int)
{
    return "";
}
//Same behavior with std::string (non primative type....)
std::string MyFunction(std::string)
{
    return "";
}

int main() {
    std::cout << HasMyFunction<int>::value << std::endl;
    std::cout << HasMyFunction<std::string>::value << std::endl;
    std::cout << HasMyFunction<Test>::value << std::endl;
    std::cout << HasMyFunction<TestBad>::value << std::endl;
}

I found a strange behavior with a custom type trait I was writing and further found that different compilers are having different behavior with the trait. The goal is simple, I want to detect if a given function MyFunction is defined/overloaded for a specific input type. I made a trait HasMyFunction to do this. The example code and godbolt link for 3 compilers behavior is below.

To me the clang and gcc behavior is surprising/incorrect. The trait returns false for primitive types and even std::string unless MyFunction for those types is defined PRIOR to the template definition for the trait. However if I use a locally defined/custom type the same trait always returns true, no matter if the class and its MyFunction is defined before or after the trait.

MSVC returns true in all cases regardless of the ordering.

My understanding is that templates don't "exist" until they are instantiated and given that the instantiation in all cases is well after all these other declarations I don't see why this ordering would matter.

What is the correct behavior here, and which of the compilers is non compliant? This does not seem like the type of thing that would be compiler dependent or left to the implementers....

Godbolt

#include <iostream>
#include <type_traits>
#include <string>

/// Helper to determine if a type has a specialization of the MyFunction function
template <typename T, typename = void>
struct HasMyFunction : public std::false_type{};

template <typename T>
struct HasMyFunction<T, std::void_t<decltype(MyFunction(std::declval<T>()))>> : public std::true_type{};


//Trait works no matter the location of this def
struct Test{};
std::string MyFunction(Test)
{
    return "";
}

struct TestBad{};

//Move me up above line 5 and the trait works as you would expect (true)
//MSVC seems to work in all cases
//gcc/clang gives false unless this is declared before the template....
std::string MyFunction(int)
{
    return "";
}
//Same behavior with std::string (non primative type....)
std::string MyFunction(std::string)
{
    return "";
}

int main() {
    std::cout << HasMyFunction<int>::value << std::endl;
    std::cout << HasMyFunction<std::string>::value << std::endl;
    std::cout << HasMyFunction<Test>::value << std::endl;
    std::cout << HasMyFunction<TestBad>::value << std::endl;
}
Share Improve this question edited Jan 17 at 19:38 I Less3 CPP asked Jan 17 at 19:33 I Less3 CPPI Less3 CPP 1215 bronze badges 5
  • 1 Looks a lot lke : std::is_invokable to me :) – Pepijn Kramer Commented Jan 17 at 21:00
  • 2 MSVC gives the same results as other compilers if you add /permissive- compiler option, but reverts to what you observed if you also add /Zc:twoPhase-. This means the lack of 2-phase lookup in MSVC by default is the reason why it differs from the other compilers. But I do not know why this behavior is correct. – Eugene Commented Jan 17 at 21:45
  • @PepijnKramer Yeah that may be an alternative workable solution, but at this point I am more interested in trying to understand what and why this is happening over swapping in a different solution. – I Less3 CPP Commented Jan 17 at 22:37
  • 1 It’s ADL (which alone can look forward of the template declaration), of course, but I can’t find the duplicate at the moment. – Davis Herring Commented Jan 17 at 23:07
  • 1 MyFunction is not a dependent name. When the compiler parses the template, it must be able to resolve the name when the template is defined. Because none of the overloads are declared at template definition, all of the decltype(...) are ill-formed. Only dependent name is resolved at instantiation. – kjpus Commented Jan 18 at 3:35
Add a comment  | 

1 Answer 1

Reset to default 2

@kjpus got me on the path to finding this with the specific mention of dependent name, but he didn't submit in the form of an answer so I am putting the answer here explicitly.

So this is due to dependent name resolution and is explained quite clearly on cppref with an example well aligned with my original example code.

Non-dependent names are looked up and bound at the point of template definition. This binding holds even if at the point of template instantiation there is a better match:

#include <iostream>
 
void g(double) { std::cout << "g(double)\n"; }
 
template<class T>
struct S
{
    void f() const
    {
        g(1); // "g" is a non-dependent name, bound now
    }
};
 
void g(int) { std::cout << "g(int)\n"; }
 
int main()
{
    g(1);  // calls g(int)
 
    S<int> s;
    s.f(); // calls g(double)
}

本文标签: gccC Type traits dependent upon declaration ordering and varying compiler behaviorStack Overflow