c++templatestemplate-specializationpartial-specialization

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


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 |


Solution

  • 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.