最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

gcc - C++ Type traits dependent upon declaration ordering and varying compiler behavior - Stack Overflow

programmeradmin4浏览0评论

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)
}
发布评论

评论列表(0)

  1. 暂无评论