看来,这里真正可以回答的问题是关于这个特性的历史,以便可以在上下文中理解任何编译器支持。
非类型模板参数类型的限制
人们已经wanting class-type non-type template parameters 很久了。那里的答案有些缺乏;真正使对此类模板参数(实际上,非平凡的用户定义类型)的支持变得复杂的是它们对identity的未知概念:给定
struct A {/*...*/};
template<A> struct X {};
constexpr A f() {/*...*/}
constexpr A g() {/*...*/}
X<f()> xf;
X<g()> &xg=xf; // OK?
我们如何判断X<f()> 和X<g()> 是否属于同一类型?对于整数,答案似乎很明显,但类类型可能类似于std::vector<int>,在这种情况下我们可能有
// C++23, if that
using A=std::vector<int>;
constexpr A f() {return {1,2,3};}
constexpr A g() {
A ret={1,2,3};
ret.reserve(1000);
return ret;
}
尽管有非常不同的行为( 例如,用于迭代器失效)。
P0732非类型模板参数中的类类型
确实,本文首先添加了对类类型非类型模板参数的支持,就新的<=> 运算符而言。逻辑是,默认该运算符的类“对比较透明”(使用的术语是“强结构相等”),因此程序员和编译器可以就身份的定义达成一致。
P1185<=> != ==
后来意识到==出于性能原因应该单独默认(例如,它允许提前退出以比较不同长度的字符串),并且重写了强结构相等的定义就该运算符而言(与默认的<=> 一起免费提供)。这不会影响这个故事,但没有它,线索是不完整的。
P1714 NTTP 不完整,没有 float、double 和 long double!
发现类类型 NTTP 和 constexpr std::bit_cast 的不相关特性允许将浮点值偷运到 std::array<std::byte,sizeof(float)> 之类的类型内的模板参数中。这种技巧产生的语义是float 的每个表示 都是不同的模板参数,尽管-0.0==0.0 和(给定float nan=std::numeric_limits<float>::quiet_NaN();)nan!=nan .因此,建议允许浮点值直接作为具有这些语义的模板参数,以避免鼓励广泛采用这种骇人听闻的解决方法。
当时,关于(给定 template<auto> int vt;)x==y 可能与 &vt<x>==&vt<y> 不同的想法存在很多混淆,并且该提案被拒绝因为需要更多分析超过了 C++20 所能承受的范围。
原来==在这方面有很多问题。即使是枚举(一直被允许作为模板参数类型)也可以重载==,并且将它们用作模板参数只会完全忽略该重载。 (这或多或少是必要的:这样的运算符可能在某些翻译单元中定义而不在其他翻译单元中定义,或者可能定义不同,或者具有内部链接,等。)此外,实现需要什么对模板参数做 canonicalize 它:比较一个模板参数(例如,调用)与另一个模板参数(例如,显式特化)需要后者已经以某种方式识别前者,同时以某种方式允许它们可能不同。
这种身份概念已经不同于 == 的其他类型。甚至 P0732 也认识到 references(也可以是模板参数的类型)不能与 == 进行比较,因为当然 x==y 并不意味着 &x==&y。不太广为人知的是,pointers-to-members 也违反了这种对应关系:由于它们在不断求值中的不同行为,指向联合的不同成员的指针作为模板参数是不同的尽管比较了==,并且已被强制转换为指向基类的指向成员的指针具有相似的行为(尽管它们的比较未指定,因此不允许作为常量评估的直接组成部分)。
事实上,在 2019 年 11 月,GCC 已经实现了对类类型 NTTP 的基本支持,不需要任何比较运算符。
P1837 从 C++20 中删除类类型的 NTTP
这些不一致的地方太多了,以至于已经有人提议将整个功能推迟直到 C++23。面对如此受欢迎的功能中存在如此多的问题,我们委托了一个小组来指定保存它所需的重大更改。
这些关于类类型和浮点类型的模板参数的故事在 P1907R0 的修订版中重新融合,该修订版保留了它的名称,但用具有 的 National Body cmets 的 解决方案替换了它的主体也就同一主题提交。 (新)想法是认识到比较从来没有真正密切相关,并且模板参数标识的唯一一致模型是,如果在持续评估期间有 any 区分它们的方法,则两个参数是不同的(具有上述区分指向成员的指针的能力,等)。毕竟,如果两个模板参数产生相同的特化,则该特化必须具有一个行为,并且它必须与直接使用任一参数所获得的行为相同。
虽然支持范围广泛的类类型是可取的,但在 C++20 几乎最后一刻引入(或者更确切地说是重写)的新特性能够可靠地支持的只有那些每个可以被实现区分的值都可以被它的客户区分——因此,只有那些具有所有 public 成员(递归地具有此属性)的值。对这种结构类型的限制不如对聚合的限制那么严格,因为任何构造过程都是允许的,只要它是 constexpr。它还为未来的语言版本提供了合理的扩展,以支持更多的类类型,甚至可能是std::vector<T>——同样,通过规范化(或序列化)而不是通过比较(不支持此类扩展)。
一般解决方案
这种新发现的理解与 C++20 中的任何其他内容都没有关系;使用此模型的类类型 NTTP 可能是 C++11 的一部分(它引入了类类型的常量表达式)。支持立即扩展到联合,但逻辑完全不限于类;它还确定,长期禁止作为子对象指针或具有浮点类型的模板参数也是出于对== 的混淆的动机,并且是不必要的。 (虽然出于技术原因,这不允许字符串文字作为模板参数,但它确实允许 const char* 模板参数指向静态字符数组的第一个字符。)
换句话说,推动 P1714 的力量最终被认为是模板基本行为的必然数学结果,浮点模板参数最终成为 C++20 的一部分。然而,C++20 的原始提案实际上并没有为 C++20 指定浮点和类类型 NTTP,这使得“编译器支持”文档变得复杂。