C++20约束偏序未含否定与德摩根律:决策原因技术问询
你观察到的这个现象完全符合C++20标准的规定——约束偏序规则确实只处理逻辑与(∧)和逻辑或(∨),完全不涉及否定(!)。咱们来拆解背后的设计考量:
先明确标准的核心规则
根据C++20标准的13.5.4约束偏序[temp.constr.order] (1.2):
- 约束
A ∧ B包含A(更严格的约束包含更宽松的),但A不包含A ∧ B - 约束
A包含A ∨ B(更宽松的约束被更严格的包含),但A ∨ B不包含A
这些规则的基础是原子约束和约束归一化(13.5.3约束归一化[temp.constr.normal]),而归一化过程明确不会处理否定表达式(!E1)——也就是说,否定会被当作一个“黑盒”原子约束的一部分,不会被展开或化简。
实际代码中的表现差异
1. 仅用AND/OR的正常偏序匹配
比如这段代码能正确匹配更严格的重载:
#include <iostream> #include <concepts> void foo(auto i) requires std::integral<decltype(i)> { std::cout << "integral 1" << std::endl; } void foo(auto i) requires std::integral<decltype(i)> && true { std::cout << "integral 2" << std::endl; } int main() { foo(0); // 输出integral 2,因为std::integral<T> ∧ true比std::integral<T>更严格 }
这里编译器能识别std::integral<T> ∧ true包含std::integral<T>,所以优先选择第二个重载。
2. 带否定的歧义情况
但一旦引入否定,哪怕逻辑上等价,编译器也无法识别偏序关系:
#include <iostream> #include <concepts> template<typename T> concept not_integral = !std::integral<T>; template<typename T> concept not_not_integral = !not_integral<T>; void foo(auto i) requires not_not_integral<decltype(i)> { std::cout << "integral 1" << std::endl; } void foo(auto i) requires std::integral<decltype(i)> && true { std::cout << "integral 2" << std::endl; } int main() { foo(0); // 编译错误:调用歧义 }
虽然not_not_integral<T>逻辑上完全等价于std::integral<T>,但归一化后两者的原子约束结构不同(一个是!not_integral<T>,另一个是std::integral<T> ∧ true),编译器无法判断谁包含谁,因此触发歧义。
3. 德摩根律的失效
同样,逻辑上等价的约束如果嵌套否定,也无法参与偏序:
// 版本1:可以正常参与偏序判断 template<class P> concept has_field_moo_but_not_foo = has_field_moo<P> && !has_field_foo<P>; // 版本2:逻辑等价,但无法参与偏序 template<class P> concept has_field_moo_but_not_foo = !(has_field_foo<P> || !has_field_moo<P>);
版本1的结构是A ∧ !B,归一化后能被编译器识别为原子约束的组合;而版本2是!(B ∨ !A),否定被当作整体,编译器不会展开德摩根律,因此它的约束结构和版本1完全不同,无法和其他约束建立偏序关系。
为什么标准要排除否定?
这个决策是实现简化和逻辑可预测性的权衡结果,主要原因有两点:
1. 编译器实现的复杂度
处理否定需要引入布尔逻辑的等价性推导,这在编译器中是极大的挑战:
- 要自动应用德摩根律、双重否定消除等规则,需要对约束表达式做深层的逻辑化简,这不仅增加了编译器的开发成本,还会带来编译性能的开销。
- 当约束涉及依赖模板参数的表达式时,逻辑等价性的判定可能变得不可判定(比如涉及运行时才能确定的值),编译器无法给出明确的结果。
标准委员会希望约束偏序的规则是简单、可预测、易于实现的,因此选择只处理最基础的AND/OR组合,原子约束的匹配仅基于替换后的完全等价性。
2. 逻辑上的反直觉性
否定约束的偏序判断容易违背开发者的直觉:
- 偏序规则的核心是“更严格的约束优先”,但否定会反转包含关系。比如
!std::signed_integral<T>逻辑上包含!std::integral<T>(所有非整数都是非有符号整数,但反之不然),但这和直觉上的“严格性”相反——开发者可能会认为!std::integral<T>是更宽泛的约束,应该被包含,而不是反过来。 - 允许否定参与偏序会让重载决议的结果变得难以预测,增加了代码的维护成本。
总结
标准委员会的选择是优先保证约束系统的可实现性和可预测性,牺牲了部分逻辑等价性的自动推导。如果你需要依赖逻辑等价的约束参与偏序,最好手动将约束转换为不含嵌套否定的形式(比如手动展开德摩根律),这样编译器才能正确识别它们的包含关系。
内容的提问来源于stack exchange,提问作者Amir Kirsh

