Template partial specialization for integral non-type parameters and non-integral non-types, difference between g++ and clang

C++TemplatesTemplate SpecializationPartial Specialization

C++ Problem Overview


The following is a simple template partial specialization:

// #1
template <typename T, T n1, T n2>
struct foo { 
    static const char* scenario() {
        return "#1 the base template";
    }
};

// #2
// partial specialization where T is unknown and n1 == n2
template <typename T, T a>
struct foo<T, a, a> { 
    static const char* scenario() {
        return "#2 partial specialization";
    }
};

The main below gets different results on g++ (6.1) and clang++ (3.8.0):

extern const char HELLO[] = "hello";
double d = 2.3;

int main() {
    cout <<   foo<int, 1, 2>                    ::scenario() << endl;                   
    cout <<   foo<int, 2, 2>                    ::scenario() << endl;                   
    cout <<   foo<long, 3, 3>                   ::scenario() << endl;                  
    cout <<   foo<double&, d, d>                ::scenario() << endl;               
    cout <<   foo<double*, &d, &d>              ::scenario() << endl;             
    cout <<   foo<double*, nullptr, nullptr>    ::scenario() << endl;   
    cout <<   foo<int*, nullptr, nullptr>       ::scenario() << endl;      
    cout <<   foo<nullptr_t, nullptr, nullptr>  ::scenario() << endl; 
    cout <<   foo<const char*, HELLO, HELLO>    ::scenario() << endl;
}

Results on g++ and clang++

# | The code | g++ (6.1) | clang++ (3.8.0) |
1 | foo<int, 1, 2> | #1 as expected | #1 as expected |
2 | foo<int, 2, 2> | #2 as expected | #2 as expected |
3 | foo<long, 3, 3> | #2 as expected | #2 as expected |
4 | foo<double&, d, d> | #1 -- why? | #2 as expected |
5 | foo<double*, &d, &d> | #2 as expected | #2 as expected |
6 | foo<double*, nullptr, nullptr> | #2 as expected | #1 -- why? |
7 | foo<int*, nullptr, nullptr> | #2 as expected | #1 -- why? |
8 | foo<nullptr_t, nullptr, nullptr> | #2 as expected | #1 -- why? |
9 | foo<const char*, HELLO, HELLO> | #2 as expected | #2 as expected |

Which one is right?

Code: https://godbolt.org/z/4GfYqxKn3


EDIT, Dec-2021:

Along the years since the original post, the results have changed, and were even identical for gcc and clang at a certain point in time, but checking again, g++ (11.2) and clang++ (12.0.1) changed their results on references (case 4), but still differ on it. It seems that currently gcc is getting it all right and clang is wrong on the reference case.

# | The code | g++ (11.2) | clang++ (12.0.1) |
1 | foo<int, 1, 2> | #1 as expected | #1 as expected |
2 | foo<int, 2, 2> | #2 as expected | #2 as expected |
3 | foo<long, 3, 3> | #2 as expected | #2 as expected |
4 | foo<double&, d, d> | #2 as expected | #1 -- why? |
5 | foo<double*, &d, &d> | #2 as expected | #2 as expected |
6 | foo<double*, nullptr, nullptr> | #2 as expected | #2 as expected |
7 | foo<int*, nullptr, nullptr> | #2 as expected | #2 as expected |
8 | foo<nullptr_t, nullptr, nullptr> | #2 as expected | #2 as expected |
9 | foo<const char*, HELLO, HELLO> | #2 as expected | #2 as expected |

C++ Solutions


Solution 1 - C++

I will dedicate my answer to case #4, because according to the OP's EDIT, the compilers now agree on cases #6-8:

># | The code | g++ (6.1) | clang++ (3.8.0) | > >4 | foo<double&, d, d> | #1 -- why? | #2 as expected |

Seems like clang++ 3.8.0 behaves correctly and gcc 6.1 rejects the perfectly fine partial specialization for this case because of the following bug that was fixed in gcc 7.2:

Bug 77435 - Dependent reference non-type template parameter not matched for partial specialization

There is a diff there with this key change in the compiler's code:

// Was: else if (same_type_p (TREE_TYPE (arg), tparm))
else if (same_type_p (non_reference (TREE_TYPE (arg)), non_reference(tparm)))

Before gcc 7.2, when a dependent type T& was matched with argument of type T in a parital specialization candidate, the compiler falsely rejected it. This behavior can be demostrated in a cleaner example:

template <typename T, T... x>
struct foo { 
    static void scenario() { cout << "#1" << endl; }
};

// Partial specialization when sizeof...(x) == 1
template <typename T, T a>
struct foo<T, a> { 
    static void scenario() { cout << "#2" << endl; }
};

In the case of T = const int the behavior of gcc 6.1 and gcc 7.2 is the same:

const int i1 = 1, i2 = 2;
foo<const int, i1, i2>::scenario(); // Both print #1
foo<const int, i1>::scenario();     // Both print #2

But in case of T = const int& the behavior of gcc 6.1 is to reject the correct partial specialization and choose the base implementation instead:

foo<const int&, i1, i2>::scenario(); // Both print #1
foo<const int&, i1>::scenario();     // gcc 6.1 prints #1 but gcc 7.2 prints #2

It affects any reference type, here are some more examples:

double d1 = 2.3, d2 = 4.6;

struct bar {};
bar b1, b2;

foo<double&, d1, d2>::scenario(); // Both print #1
foo<double&, d1>::scenario();     // gcc 6.1 prints #1 but gcc 7.2 prints #2
foo<bar&, b1, b2>::scenario();    // Both print #1
foo<bar&, b1>::scenario();        // gcc 6.1 prints #1 but gcc 7.2 prints #2

You can run this example here: https://godbolt.org/z/Y1KjazrMP

gcc seems to make this mistake up to gcc 7.1 but from gcc 7.2 up to the current version it chooses the partial specialization correctly because of the bugfix above.

In conclusion, case #4's result in the question is just a symptom of a more general problem and it happens just because double& is a reference type. To demonstrate this claim, try to add the following line in the OP's code (and bar, b1 definitions from my example):

cout << foo<bar&, b1, b1>::scenario() << endl;

and observe that gcc 6.1 prints again "#1 the base template" while gcc 7.2 and onward prints "#2 partial specialization" as expected.


EDIT

Regarding the follow-up question in the OP's EDIT:

># | The code | g++ (11.2) | clang++ (12.0.1) | > >4 | foo<double&, d, d> | #2 as expected | #1 -- why? |

I think that g++ (11.2) is correct.

Pay attention that clang wasn't flipped its answer entirely because in your link, you have used c++20 standard but if you change it back to c++14 as in the original question, even clang++ 12.0.1 agrees with g++ 11.2 and chooses the parial specialization.

Actually, it happens with clang in c++17 too and it seems to be an issue in clang that started with this standard and isn't fixed until today.

If you try to add the following test case to your code:

TEST (foo<const int, 2, 2>); // clang (c++17/20) prints #1 and gcc (any) prints #2

clang also chooses the base template instead of the partial specialization like gcc while in this test case:

TEST (foo<int, 2, 2>); // Both agree on #2

they both agree, which I find odd, because this added const to the type shouldn't impact the partial specialization's fitness and it seems like clang doesn't do that for references only, but for consts as well! and only when the standard >= C++17.

BTW, this issue can be reproduced in my example too: https://godbolt.org/z/W9q83j3Pq

Observe that clang 8.0.0 disagrees with itself just by changing the language standard and it keeps doing that up to clang 13.0.0 even on such a simple case when no argument value equality required.

These odd template deductions in clang raise enough "red flags" so I must conclude that g++ (11.2) is correct.

My wild guess is - C++17 introduced CTAD, which make clang act differently with class template deduction, and this issue is somehow connected to its new implementation while the old C++14 implementation stayed intact.

Solution 2 - C++

Let's begin with determining what is correct according to the C++ standard, and compare that to the actual compilers. I am pretty sure, that it should work the same, as you expect, i.e. #1 for the first case and #2 for all the rest (although some caveats apply, see later).

As evzh pointed out in his comment (Jul 1 2020 at 3:30), gcc 7.2 and clang 4.0.0 are the earliest versions that work correctly. They work correctly all the way up to the latest versions with -std=c++11.

And here is caveat. Clang works differently when you increase -std from c++14 to c++17. In the question in EDIT, Dec-2021 you are using -std=c++20, which is after c++17. That is why you experienced the why? case.

My assumption is that the compilers had bugs in 2016 where they differed from the above established correct behavior, and clang 12.0.1 with -std=c++17 or greater still has a bug in case of foo<double&, d, d>.

Clang is a very pedantic compiler, so I might be wrong. There is a little chance, that something changed in the standard, that makes clang correct and the other compilers wrong. This is worth being asked in a separate question.

Here is a simplified test to demonstrate all your cases: demo. I have added four compilers to show how foo<double&, d, d> behaves.

Solution 3 - C++

#4 is ill-formed, I'm surprised it compiles. For one thing, a double can't be used as a non-type template parameter. For another thing, a class template uses the same rules as function templates for partial ordering. The standard provides an example of the "imaginary" functions generated to perform the ordering:

template<int I, int J, class T> class X { };
template<int I, int J> class X<I, J, int> { }; // #1
template<int I>        class X<I, I, int> { }; // #2
template<int I, int J> void f(X<I, J, int>); // A
template<int I>        void f(X<I, I, int>); // B

For your example, it would look something like this:

template <typename T, T n1, T n2>
struct foo { 
};

template <typename T, T n1, T n2>
void bar(foo<T, n1, n2>)
{
    std::cout << "a";
}

template <typename T, T a>
void bar(foo<T, a, a>)
{
    std::cout << "b";
}

Template argument deduction is used to determine which function is more specialized than the other. A double& should be deduced as a double and so both specializations should be equal, aka ambiguous for bar(foo<double&, d, d>{});. Lo and behold GCC and Clang complain:

> GCC error > > main.cpp:14:6: note: template argument deduction/substitution > failed: main.cpp:26:29: note: mismatched types 'double&' and > 'double' > > bar(foo{}); > ^ > > Clang error > > note: candidate template ignored: substitution failure [with T = > double &]: deduced non-type template argument does not have the same > type as the its corresponding template parameter ('double' vs 'double > &')

And again, if you remove the reference, they both correctly complain about using double as a non-type template parameter.

I'm not going to test the rest, but you may find similar results.

Solution 4 - C++

I took the latest compilers and tried to explain the case of foo behaver.

The foo function is declared as follow

// #1
template <typename T, T n1, T n2>
struct foo { 
    static const char* scenario() {
        return "#1 the base template";
    }
};

// #2
// partial specialization where T is unknown and n1 == n2
template <typename T, T a>
struct foo<T, a, a> { 
    static const char* scenario() {
        return "#2 partial specialization";
    }
};

The issue here is that the T type gets deduced by the call immediately as double and not double& as expected, and then it is implicit cast to double &. That's why the different compilers behave in a different way. changing the foo function to the following will solve the issue.

// #1
template <typename T, typename std::type_identity<T>::type n1, typename std::type_identity<T>::type n2>
struct foo { 
  static const char* scenario() {return "#1 the basic: template <typename T, T n1, T n2>";}
};

// #2
// partial specialization where T is unknown and n1 == n2
template <typename T, typename std::type_identity<T>::type a>
struct foo<T, a, a> { 
  static const char* scenario() {return "#2 partial specialization: template<typename T, T a> foo<typename T, a, a>";}
};

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAmir KirshView Question on Stackoverflow
Solution 1 - C++Tal SaridView Answer on Stackoverflow
Solution 2 - C++Dr. GutView Answer on Stackoverflow
Solution 3 - C++user6366161View Answer on Stackoverflow
Solution 4 - C++Avi LachmishView Answer on Stackoverflow