精灵湾水上乐园新开园时,每个游乐设施的入口都只是一扇铁皮门,没有任何说明。游客在烈日下排一小时队,坐上设施转了两圈,才被卡在半空中——广播里滚出一长串听不懂的术语,始终没说清“你究竟哪里不合格”。后来园方终于醒悟:在每个设施门口立一块“游玩须知牌”,身高不低于 120 厘米、需成人陪同、禁止穿拖鞋……游客站在闸机前就能看清自己够不够格,不合格的连队都不用排。 C++20 的 Concepts(模板约束)就是给模板参数立这样的须知牌。在它出现之前,C++ 模板就像一个把检查藏在设施深处的暗桩区:你传入一个错误类型,编译器在模板实例化内部绕几十圈,最后抛出一页天书般的错误信息,却没说清“你缺的到底是哪一项”。有了 Concepts,类型要求被写成显式、编译期就能求值的“概念”,不满足时报错直接点名:传进来的类型违反了哪个概念、差在哪里。 本文就用这座水上乐园当主线:先看概念是什么、怎么写,再学 requires 表达式这张“检查清单”,认识标准库现成的“统一闸机”,对比旧式 SFINAE 的暗桩玩法,学会把多条约束组合成安检流水线,最后看真实项目如何完成全园改造。

岗位关联

对 AI 从业者而言,这是迈进高性能推理框架与训练系统源码的必备钥匙:用 Concepts 把模板代码的检查提前到编译期,错误信息从三页天书变一行人话,读 PyTorch、CUDA 生态的 C++ 代码不再犯怵。

目录
  1. 可视化演示
  2. Concepts是什么
  3. requires表达式
  4. 标准库概念
  5. 对比SFINAE
  6. 多约束组合
  7. 实战模板改造
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示concept定义语法、requires表达式、标准库概念与SFINAE对比。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Concepts是什么

精灵湾水上乐园以前每个项目入口都只是锈铁门,门后藏多少检查谁也不知道。园方痛定思痛,决定在每台设施门口立起“游玩须知牌”,把硬性要求写到明面上:身高多少、会不会游泳、能否更换装备。游客站在闸机前,一眼就能判断自己够不够格。

这块须知牌对应到 C++ 里就是 Concept(概念)。模板函数和模板类就是那些游乐设施,模板参数 T 就是排队的游客。concept 用一段编译期就能算出的布尔表达式,描述“什么样的类型 T 才配坐这台设施”。它把原本藏在模板深处的隐性假设,翻译成门口的一条条明文条款。

最基础的定义语法只有三行:先用 template<typename T> 引入受检的类型形参;再用 concept 关键字给这块须知牌起名;最后用 = 接一个编译期常量表达式,通常是类型萃取或 requires 表达式,计算结果是 true 或 false。例如 template<typename T> concept Integral = std::is_integral_v<T>; 就定义了一个“只允许整数类型入场”的概念。

须知牌立好,闸机启动:游客还没跨进大门,体检就已经完成。不合格的连队都不用排,这正是 Concepts 给模板世界带来的第一份礼物——把“排到一半才发现不行”变成“进门之前就知道结果”。

concept C 对类型 T:C(T)≡⋀i=1nPi(T)\displaystyle \text{concept}\ C\ \text{对类型}\ T:\quad C(T) \equiv \bigwedge_{i=1}^{n} P_i(T)
用户视角:一个概念 C 就是一台“类型体检机”,把类型 T 喂进去,输出一个编译期的 true/false;多条要求用“且”串起来,全部合格才算通过。
  1. 概念名 CC 是概念的名字,等价于一个以类型为参数的编译期谓词。
    C(T): T⟶{true, false}C(T):\ \mathcal{T} \longrightarrow \{\text{true},\ \text{false}\}
  2. 合取符号 ∧右侧多条要求必须同时成立,缺一不可。
    P1(T)∧P2(T)∧⋯∧Pn(T)P_1(T) \wedge P_2(T) \wedge \cdots \wedge P_n(T)
  3. 编译期求值C(T) 在编译期就确定,不产生任何运行时代码。
    C(T)=常量∈{true, false}C(T) = \text{常量} \in \{\text{true},\ \text{false}\}

requires表达式

有些技术要求一句话说不清。须知牌背面必须附一张详细检查清单:“必须能自己爬梯子”“必须穿防滑胶鞋”“必须由成人陪同”。requires 表达式就是这张清单:把想检查的动作逐条列出来,编译器逐项打勾,全部通过清单才算 true。

它的语法看起来像函数体,但含义完全不同。requires(T a, T b) { a + b; } 是在问:给定两个类型 T 的对象,a + b 这个表达式能不能通过编译?能,这一项打勾;不能,整张清单作废。清单里还能写类型要求,比如 typename T::value_type;,检查 T 是否真的有这个成员类型。

requires 表达式可以直接写在 concept 定义的右侧,构成“内嵌检查清单”;也可以单独作为约束子句出现在函数模板上。它只问“合法性”,不问“结果值”:a + b 不会被真的执行,编译器只验证它是否是一句合法的表达式,就像安检员只看你有没有带违禁品,不会把每件行李都拆开试用一遍。

闸机对照清单逐条打勾,而不是把游客放进去试坐。这张“检查清单”就是 concept 定义里最常用的引擎,它让“能编译”这三个字,变成可组合、可阅读、可传递的一等公民。

requires(T) { e1; e2; ⋯ }  ⟺  ⋀i合法(ei)\displaystyle \text{requires}(T)\ \{\ e_1;\ e_2;\ \cdots\ \} \iff \bigwedge_{i} \text{合法}(e_i)
用户视角:requires 表达式是“检查清单模式”,里面的每一项都只是一道是非题:这条表达式能否编译通过?全部回答“能”,整体结果才是 true。
  1. requires 关键字声明这里开启一张编译期检查清单,受检对象是类型 T。
    requires(T x)\text{requires}(T\ x)
  2. 清单项 e_i每一项是一条表达式或一个类型名,分号结尾。
    ei∈{ x+1,  typename T::value_type,  … }e_i \in \{\ x + 1,\ \ \text{typename}\ T::value\_type,\ \ \dots \ \}
  3. 合取判断任何一项不合法,整张清单返回 false。
    ⋀i合法(ei)\bigwedge_{i} \text{合法}(e_i)
  4. 只验证不执行仅做编译期合法性检查,不生成运行指令,也没有副作用。

标准库概念

如果每家设施都自己画身高尺,游客换个项目就要重新适应。于是园方统一配发了标准闸机套件:身高尺(integral)、体重秤(floating_point)、身份核验(same_as)、换乘通道(convertible_to)。大家用同一套刻度,规则不再各说各话。

C++20 的 <concepts> 头文件就是这套标准套件。std::integral<T> 判断 T 是不是整数家族成员;std::floating_point<T> 判断是不是浮点家族;std::same_as<T, U> 要求两个类型完全相同;std::convertible_to<From, To> 检查 From 能否合法转换成 To。此外还有 equality_comparable(可比较相等)、invocable(可被调用)等一批描述关系与能力的标准概念。

这些标准概念可以直接拿来当须知牌用:template<std::integral T> T twice(T x); 就能把函数锁定在整数类型上。它们内部大多由类型萃取实现,但披上了“概念”的外衣后,可读性发生了质变——约束名就是需求本身,读代码的人不再需要去猜那一串 enable_if 在干什么。

统一闸机让全园标准一致,游客游玩预期稳定。标准库概念的价值就在于此:绝大多数常见约束不必自己发明,直接引用标准件,团队内沟通成本直线下降。

integral(T)  ⟺  T∈{bool, char, int, long, … }convertible_to(F,T)  ⟺  F↪T\displaystyle \text{integral}(T) \iff T \in \{\text{bool},\ \text{char},\ \text{int},\ \text{long},\ \dots\} \qquad \text{convertible\_to}(F,T) \iff F \hookrightarrow T
用户视角:integral 是“身份名录”判定,convertible_to 是“换乘通道”判定。前者查户籍,后者查有没有一条合法的转换路线。
  1. integral 名录判定只认整数家族的成员类型,浮点与自定义类型一律为 false。
    T∈{bool, char, int, long, … }T \in \{\text{bool},\ \text{char},\ \text{int},\ \text{long},\ \dots\}
  2. convertible_to 换乘箭头从 F 到 T 存在合法的隐式转换路径。
    F↪T: 转换表达式可编译F \hookrightarrow T:\ \text{转换表达式可编译}
  3. same_as 严格相等两个类型完全相同才返回 true,杜绝“差不多”。
    U≡T ⇒ same_as⟨T,U⟩=trueU \equiv T \ \Rightarrow\ \text{same\_as}\langle T, U\rangle = \text{true}

对比SFINAE

改造前的精灵湾没有须知牌,只有“坏了再说”的暗桩。模板实例化时发现类型不满足某些操作,就悄悄把该重载“隐身”,让编译器再去别处找——如果最终找不到,报错信息层层嵌套堆成天书,最后一行才藏着“没有匹配的函数”。这套老机制叫 SFINAE(替换失败不是错误)。

旧式写法用 std::enable_if 加类型萃取手动拼条件:template<typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0> void f(T) {}。它能在“类型不对”时把函数藏起来,但藏起来之后发生什么,全靠编译器自己摸索。而 Concepts 的写法 template<std::integral T> void f(T) {} 同样能把不合适的重载移出候选集,区别在于:失败时编译器直接亮出概念名,告诉你“这个类型违反了 Integral 约束”。

两者的共同点是都发生在编译期、都参与重载决议;本质差异在失败的可视化。SFINAE 是“默默离场,留下天书”,Concepts 是“当场拦下,点名批评”。这也是 Concepts 被称为“模板的自我文档化”的原因——约束即文档。

旧乐园让游客默默离场,新乐园对游客说清“你缺什么”。从 SFINAE 到 Concepts 不是推翻重来——它是同一件事的两种语气:一种是“反正不行,你自己悟”,一种是“你不行,因为你差在这里”。

旧式: fSFINAE(T)→ ¬P(T) 无此重载Concepts: f(T)→ ¬C(T) 约束未满足,报错点名\displaystyle \text{旧式:}\ f_{\text{SFINAE}}(T) \xrightarrow{\ \neg P(T)\ } \text{无此重载} \qquad \text{Concepts:}\ f(T) \xrightarrow{\ \neg C(T)\ } \text{约束未满足,报错点名}
用户视角:SFINAE 让不合格的候选函数“假隐身”,编译器在迷雾里乱转;Concepts 让不合格的候选函数“亮红灯”,报错信息直接写着违反哪条约束。
  1. 否定条件 ¬P(T)旧式通过 enable_if 让条件不满足时函数重载消失。
    ¬P(T) ⇒ enable_if 不提供该重载\neg P(T)\ \Rightarrow\ \text{enable\_if 不提供该重载}
  2. 隐身与亮灯SFINAE 把重载移出候选集,Concept 则在诊断信息里点名。
    隐身: 候选集缩小亮灯: 错误里出现概念名\text{隐身: 候选集缩小} \qquad \text{亮灯: 错误里出现概念名}
  3. 可读性差异概念名直接成为错误信息的一部分,定位问题从小时级降到分钟级。
    error: constraint C⟨T⟩ not satisfied\text{error: constraint } C\langle T\rangle \text{ not satisfied}

多约束组合

须知牌很少只写一条规则。园方很快学会了组合:并列条件用“且”,互替条件用“或”,还可以在函数尾部追加补充条款。C++ 里对应的是概念的逻辑组合运算符。

requires C1<T> && C2<T> 要求两个概念同时满足,像安检流水线必须连过两道闸;C1<T> || C2<T> 允许满足其一即可,像“持年卡或当日票都能进”。此外,约束还能后置:template<class T> void f(T) requires C1<T> && C2<T>;,把条件写在函数声明末尾,读起来像一份补充说明。

更妙的是,多约束组合还能参与重载决议:编译器会优先选择约束更严格的重载,就像“精通 C++ 且熟悉 CUDA”的候选人会比只满足“熟悉 C++”的候选人排位更前。约束合取使用短路语义,与运算符两侧都能放概念名或 requires 表达式,构成任意复杂的安检流水线。

多条闸机串成安检流水线,规则越复杂,门口的牌子反而越清晰。多约束组合让概念从“单条禁令”升级为“整套规程”,是实战中把约束写进真实代码的必备技能。

(C1∧C2)(T)  ⟺  C1(T)∧C2(T)(C1∨C2)(T)  ⟺  C1(T)∨C2(T)\displaystyle (C_1 \wedge C_2)(T) \iff C_1(T) \wedge C_2(T) \qquad (C_1 \vee C_2)(T) \iff C_1(T) \vee C_2(T)
用户视角:与运算就是“两道闸都要过”,或运算就是“二选一即可”。概念可以被当成布尔值做逻辑运算,再塞回模板参数里。
  1. 与约束 ∧&& 连接两个概念,全部为 true 才放行。
    C1(T)∧C2(T)⇒通过C_1(T) \wedge C_2(T) \Rightarrow \text{通过}
  2. 或约束 ∨|| 连接两个概念,任一成立即放行。
    C1(T)∨C2(T)⇒通过C_1(T) \vee C_2(T) \Rightarrow \text{通过}
  3. 后置 requires 子句函数声明尾部追加约束,逻辑与写在尖括号里等价。
    template⟨T⟩ void f(T)  requires C1⟨T⟩∧C2⟨T⟩\text{template}\langle T\rangle\ \text{void } f(T)\ \ \text{requires}\ C_1\langle T\rangle \wedge C_2\langle T\rangle

实战模板改造

全园改造完工后,每个项目门口都不再裸奔。函数模板、类模板、甚至 auto 占位符,全部统一换成带约束的闸机。游客体验焕然一新,投诉率直线下降。

函数模板的改造最直接:template<std::integral T> T twice(T x) 替代了旧的 enable_if 长签名,传 double 进来,报错直接说“double 不满足 integral”。类模板同样受益:template<std::convertible_to<double> T> class Scalar; 能在类定义入口就把不合规的类型挡在门外。更省事的写法是“受限 auto”:void print(std::integral auto x) 直接用概念修饰参数,连 template 关键字都省了,签名本身就是文档。

真实项目里,这套改造带来的最大收益不是代码变短,而是错误信息变短。曾经模板实例化失败时,错误输出可能横跨几百行,改造成 Concepts 后,第一行就写明违反哪个概念。团队新人也能直接读懂约束意图,代码评审从“猜这个 enable_if 在防什么”变成“看概念名就知道契约是什么”。

全园统一闸机后,游客满意度报表直线上升。Concepts 从不是花哨的语法游戏,它是把模板代码的隐性契约搬上明面:约束写在哪,错误就亮在哪,文档就长在哪。

template⟨Concept T⟩  f(T x)≡template⟨T⟩  f(T x) requires Concept⟨T⟩\displaystyle \text{template}\langle \text{Concept}\ T\rangle\ \ f(T\ x) \equiv \text{template}\langle T\rangle\ \ f(T\ x)\ \text{requires}\ \text{Concept}\langle T\rangle
用户视角:尖括号里写概念名是“简洁闸机”,函数尾部写 requires 是“补充闸机”,受限 auto 是“迷你闸机”。三者在编译期语义上等价。
  1. 受限模板形参concept 直接出现在尖括号中,声明 T 必须满足该概念。
    template⟨Concept T⟩\text{template}\langle \text{Concept}\ T\rangle
  2. 与后置等价两种写法在编译器眼里完全等价,只是语法糖不同。
    Concept⟨T⟩  ⟺  requires Concept⟨T⟩\text{Concept}\langle T\rangle \iff \text{requires}\ \text{Concept}\langle T\rangle
  3. 受限 auto参数直接写 概念 auto,函数自动成为模板,签名即约束。
    f(integral auto x)⇒f 只在 x 为整数时可用f(\text{integral auto}\ x) \Rightarrow f \text{ 只在 } x \text{ 为整数时可用}

AI 应用场景

AI推理框架的数值类型约束

阶段:模型推理引擎的 C++ 内核开发

解决的问题:AI 推理框架里张量运算要求元素必须是数值类型。传统做法在模板内堆一长串 static_assert 或 enable_if,团队新增一个半精度类型时,错误信息在一堆模板实例化中迷失,新人往往要花半天定位“到底哪个类型不合法”。引入 Concepts 后,用 template<std::floating_point T> class Tensor 在入口处把关,类型不合法时编译错误直接点名。

具体案例:某推理引擎在移植到 ARM 平台时,需要支持 half 类型。工程师定义 concept HalfSupported = std::floating_point<T> || std::same_as<T, half>;,所有算子模板统一改用该约束,编译错误从几百行缩到几行,调试时长从一天降到一小时。

推荐算法库的迭代器约束

阶段:特征工程与在线服务的算法库重构

解决的问题:推荐系统的特征拼接代码大量使用 std::ranges 算法,旧代码靠 SFINAE 悄悄隐藏不满足随机访问要求的重载,出错时“没有匹配的函数”让调用方不知所措。用 std::random_access_iterator 概念约束算法模板后,调用方传错迭代器类型,编译错误直接指出缺了哪种迭代器能力。

具体案例:某广告排序服务在将特征查询逻辑泛型化时,把三个依赖 enable_if 的排序函数改写成 template<std::random_access_iterator It> void batch_sort(It first, It last);。重构后代码行数减少四成,线上编译失败率下降,新同学也能独立看懂约束契约。

Python 演示

python52 行
class ConceptGate:    """模拟 C++20 的 concept 闸机: 编译期谓词 + 多规则合取"""     def __init__(self, name):        self.name = name        self._check_count = 0     def check(self, value, *rules):        self._check_count += 1        passed = all(rule(value) for rule in rules)        if not passed:            raise TypeError("reject: type does not satisfy " + self.name)        return True     @property    def check_count(self):        return self._check_count  def rule_integral(v):    return isinstance(v, int) and not isinstance(v, bool)  def rule_convertible_to_double(v):    return isinstance(v, (int, float))  gate = ConceptGate("Doubleable")  def with_gate(g):    def decorator(fn):        def wrapper(arg):            g.check(arg, rule_integral, rule_convertible_to_double)            return fn(arg)        return wrapper    return decorator  @with_gate(gate)def halve(x):    """把整数或浮点减半, 字符串等会被概念闸机拦下"""    return x / 2  if __name__ == "__main__":    print(halve(6))    try:        halve("6")    except TypeError as exc:        print(exc)    print("check_count =", gate.check_count)
目的:直观演示“概念 = 一组编译期规则的合取”:满足全部规则的类型放行,违反任一规则立即被拦截,并收到一条可读的拒绝信息。
  1. class ConceptGate:模拟“概念闸机”,持有概念名与累计检查次数,对应 C++ 的 concept。
  2. self._check_count = 0用下划线后缀的私有计数,防止与 check 方法同名冲突。
  3. def check(self, value, *rules):闸机核心:对所有规则做合取(all),一旦有规则不满足就抛出 TypeError。
  4. all(rule(value) for rule in rules)对应 requires 表达式的“逐条打勾、全部通过才为 true”。
  5. def rule_integral(v):模拟标准库 std::integral:只认 int,不认 bool(bool 是 int 子类但语义不同)。
  6. def rule_convertible_to_double(v):模拟 std::convertible_to:int 和 float 都允许转换成 double。
  7. @with_gate(gate)装饰器把函数改造成“受约束的模板”,类似 template<Doubleable T>。
  8. print("check_count =", gate.check_count)展示闸机被调用的次数,强调检查发生在“进入函数体之前”。

常见误区

误区concept 写起来像一段返回 bool 的代码,初学者以为它在程序运行时对类型做判断,就像 `if (type == int)` 那样,程序跑起来才检查。

正解concept 是纯编译期机制,它在编译阶段就完成全部求值,不生成任何运行时代码,也不产生运行时开销。就像水上乐园的量身高尺,在你买票进园前就量完了,而不是等你坐上设施才拿尺子量。

为何常见:concept 的语法太像普通函数,而 is_integral_v<int> 这种调用形式又容易让人联想到运行时函数调用。

误区看到 `requires(T x) { x + 1; }`,会以为程序会把 x 加一,像执行普通代码一样产生副作用。

正解requires 表达式只做“编译期合法性审查”:编译器检查 x + 1 是不是一句能编译通过的表达式,不生成任何执行指令。它问的是“语法上允许吗”,不是“运行结果是什么”。

为何常见:requires 的写法有花括号、有分号,看起来跟函数体一模一样,大脑默认把它当成可执行代码。

误区学会了 Concepts 就觉得 `enable_if`、`is_integral_v` 全是老古董,应该彻底删掉。

正解concept 内部恰恰经常借助类型萃取来实现,比如 integral 就是包了一层 is_integral_v。SFINAE 也仍用于检测函数签名等精细场景。Concepts 是“更好的封装与表达”,不是“物理上的替代”。

为何常见:技术宣传往往夸大新特性,让人误以为新语法会淘汰所有旧工具,忽略了它们其实是上下层协作关系。

误区既然 concept 右侧是表达式,那是不是可以在里面调用 `std::string::size()` 甚至用户自定义函数?

正解concept 右侧必须是编译期常量表达式,结果在编译期就要能确定。任何依赖运行时数据(如字符串内容、文件状态、时钟)的调用都不能出现在概念定义中。须知牌上的规则必须一眼就能判定,不能“坐下转两圈才知道行不行”。

为何常见:模板代码中“编译期可求值”与“运行时可调用”边界模糊,初学者容易把两者混为一谈。

面试题

Q1. concept 的类型校验发生在哪个阶段?

单选题 · easy

  • A.程序运行期间
  • B.代码编译期间← 正确答案
  • C.目标链接期间
  • D.处理器执行期间

答案:B。concept 是编译期机制,和模板实例化同时完成,运行时毫无存在感。就像水上乐园的身高测量在进门闸机处完成,而不是等游客坐上游乐设施才量。

Q2. 定义 concept 时必须使用哪个关键字?

单选题 · easy

  • A.template 关键字
  • B.constexpr 关键字
  • C.concept 关键字← 正确答案
  • D.requires 关键字

答案:C。定义概念用 concept 关键字,形如 template<typename T> concept C = ...;。requires 用于写检查表达式或约束子句,constexpr 用于编译期函数,都不负责“声明概念”。

Q3. template<std::integral T> void f(T) 中 Integral 的作用是什么?

单选题 · mid

  • A.规定 T 是整数类型← 正确答案
  • B.规定 T 是浮点类型
  • C.规定 T 能比较大小
  • D.规定 T 能隐式转换

答案:A。std::integral 是标准库概念,专门检查 T 是否为 bool、char、int、long 等整数家族成员。它像须知牌上的“身高不低于 120 厘米”条款,浮点类型和自定义类型都会被闸机拦下。

Q4. requires 表达式里的 typename T::value_type; 表示什么要求?

单选题 · mid

  • A.要求存在成员类型← 正确答案
  • B.要求表达式能编译
  • C.要求类型必须是整型
  • D.要求类型能打印

答案:A。这条是“类型要求”:检查 T 内部是否真的存在名为 value_type 的成员类型。它不关心该类型是什么,只关心“存不存在”,对应须知牌上“必须配备防滑胶鞋”这类条目。

Q5. std::integral<double> 的结果是什么?

单选题 · mid

  • A.编译期结果为真
  • B.编译期结果为假← 正确答案
  • C.引发编译错误
  • D.运行时才能判断

答案:B。double 是浮点类型,不在整数家族名录里,integral<double> 在编译期就求值为 false。它不是错误,只是闸机判定“此类型不满足入场要求”。

Q6. 关于 std::convertible_to<From, To>,下列说法正确的有?

多选题 · mid

  • A.要求 From 能转换成 To← 正确答案
  • B.常用于约束函数参数← 正确答案
  • C.与 same_as 概念完全相同
  • D.编译期检查转换合法性← 正确答案

答案:ABD。convertible_to 检查从 From 到 To 的转换是否合法,是编译期检查,常用来约束函数参数。它只要求“能转换”,与要求“两个类型完全相同”的 same_as 不是一回事。

Q7. 旧式 SFINAE 搭配 enable_if 的主要缺陷是什么?

单选题 · hard

  • A.不支持编译期校验
  • B.失败时错误信息难懂← 正确答案
  • C.运行时性能开销大
  • D.只能用于类模板

答案:B。enable_if 同样在编译期工作且零开销,但它失败时重载悄悄消失,编译器在候选集里绕半天才给出“没有匹配的函数”,错误信息堆成天书。Concepts 则直接点名违反哪个概念,诊断体验天差地别。

Q8. 关于多约束组合 C1<T> && C2<T> 与 C1<T> || C2<T>,正确的有?

多选题 · hard

  • A.与运算要求两约束同时成立← 正确答案
  • B.或运算满足一个即可成立← 正确答案
  • C.约束也会参与重载决议← 正确答案
  • D.约束求值发生在运行时

答案:ABC。&& 是全过才放行,|| 是一过即放行,两者都在编译期求值。编译器还会根据约束强弱选择更合适的重载,所以 D 的“运行时求值”明显错误。

Q9. 关于 requires 表达式能检查的内容,正确的有?

多选题 · hard

  • A.表达式能否编译通过← 正确答案
  • B.类型别名能否存在← 正确答案
  • C.表达式返回真或假
  • D.表达式结果满足概念← 正确答案

答案:ABD。requires 表达式检查“表达式合法性”“类型是否存在”“返回类型是否满足某概念”,但它只问“能不能编过”与“返回类型是什么”,从不执行表达式、也不看运行结果值,所以 C 错误。

Q10. 简化形式 template<C T> void f(T) 中的 C 允许是什么?

单选题 · easy

  • A.任意的运行时函数
  • B.一个已定义的概念← 正确答案
  • C.任意类型萃取工具
  • D.任意类模板特化

答案:B。尖括号里直接写名字的写法要求 C 必须是一个已定义的概念(concept),不能是普通函数或单独的萃取工具。概念就是那张写好的须知牌,闸机直接引用它即可。