模板部分特化用于积分非类型参数和非积分非类型,g ++和clang之间的区别

Ami*_*rsh 19 c++ templates partial-specialization template-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";
    }
};
Run Code Online (Sandbox Code Playgroud)

下面的主要内容在g ++(6.1)和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;
}
Run Code Online (Sandbox Code Playgroud)

关于g ++和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 |

哪一个是对的?

代码:http://coliru.stacked-crooked.com/a/45ba16c9f021fd84

Tal*_*rid 6

我将把我的答案献给案例#4,因为根据OP的编辑,编译器现在同意案例#6-8:

# | The code | g++ (6.1) | clang++ (3.8.0) |

4 | foo<double&, d, d> | #1 -- why? | #2 as expected |

看起来clang++ 3.8.0行为正确并且gcc 6.1拒绝了这种情况下完美的部分特化,因为以下错误已修复gcc 7.2

Bug 77435 - 依赖引用非类型模板参数与部分专业化不匹配

编译器代码中的这一关键更改存在差异

// Was: else if (same_type_p (TREE_TYPE (arg), tparm))
else if (same_type_p (non_reference (TREE_TYPE (arg)), non_reference(tparm)))
Run Code Online (Sandbox Code Playgroud)

之前gcc 7.2,当依赖类型与部分特化候选中的T&类型参数匹配时,编译器会错误地拒绝它。T这种行为可以用一个更清晰的例子来演示:

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; }
};
Run Code Online (Sandbox Code Playgroud)

在和T = const int的行为相同的情况下:gcc 6.1gcc 7.2

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

但如果 的T = const int&行为gcc 6.1是拒绝正确的部分特化并选择基本实现:

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
Run Code Online (Sandbox Code Playgroud)

它会影响任何引用类型,以下是更多示例:

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
Run Code Online (Sandbox Code Playgroud)

您可以在此处运行此示例:https ://godbolt.org/z/Y1KjazrMP

gcc似乎会犯这个错误,gcc 7.1但从gcc 7.2当前版本开始,由于上面的错误修复,它正确地选择了部分专业化。

总之,问题中案例 #4 的结果只是一个更普遍问题的症状,它的发生只是因为double&是引用类型。为了证明这一说法,请尝试在OP的代码中添加以下行(以及barb1的示例中的定义):

cout << foo<bar&, b1, b1>::scenario() << endl;
Run Code Online (Sandbox Code Playgroud)

并观察在按预期打印时gcc 6.1再次打印。"#1 the base template"gcc 7.2"#2 partial specialization"


编辑

关于OP编辑中的后续问题:

# | The code | g++ (11.2) | clang++ (12.0.1) |

4 | foo<double&, d, d> | #2 as expected | #1 -- why? |

我认为这g++ (11.2)是正确的。

请注意,clang它的答案并没有完全翻转,因为在您的链接中,您已经使用了c++20标准,但是如果您将其改回c++14原始问题中的那样,甚至clang++ 12.0.1同意g++ 11.2并选择部分专业化。

实际上,它也发生在clangin 中c++17,这似乎是从clang该标准开始的一个问题,直到今天才得到解决。

如果您尝试将以下测试用例添加到您的代码中:

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

clang还选择基本模板而不是像gcc此测试用例中的 while 那样的部分专业化:

TEST (foo<int, 2, 2>); // Both agree on #2
Run Code Online (Sandbox Code Playgroud)

他们都同意,我觉得很奇怪,因为添加const到类型中的这不应该影响部分特化的适应性,而且看起来clang这样做不仅仅为了参考,而且对于常量也是如此!并且仅当标准 >= C++17 时。

顺便说一句,这个问题也可以在我的示例中重现: https ://godbolt.org/z/W9q83j3Pq

观察一下,clang 8.0.0仅仅通过改变语言标准,它就会与自己不一致,并且它会一直这样做,clang 13.0.0甚至在不需要参数值相等的简单情况下也是如此。

这些奇怪的模板推论clang引发了足够的“危险信号”,所以我必须得出结论,这g++ (11.2)是正确的。

我的大胆猜测是 - C++17 引入了CTAD,这使得clang类模板推导的行为有所不同,并且这个问题在某种程度上与其新实现有关,而旧的 C++14 实现保持不变。

  • 有趣的!干得好,塔尔,这是对海湾合作委员会历史的一次非常彻底的调查,指出了做出改变的实际修复。更有趣的是理解为什么 clang 在已经符合参考的预期结果之后又恢复到看起来不好的行为。 (2认同)