第 17 章

模板与 Concepts:从函数蓝图到编译期约束

函数模板不是代码,而是编译器据以生成实际函数的蓝图;类型推导决定生成哪一份实例,显式模板实参可以覆盖它,特化可以替换默认实现,concepts 则把「这个类型必须支持什么」写进签名。

Templates and Concepts: From Function Blueprints to Compile-Time Constraints

A function template is not code but a blueprint the compiler instantiates into real functions. Deduction decides which instance is generated, explicit template arguments override it, specialization replaces the default implementation, and concepts write the type's requirements into the signature.

首发于 2022.02.17中 / EN
系统课程cpptemplatesconceptstemplate-specializationcpp20
语言
字号
目录
篇内目录

这是 31 小时长视频的第 17 章,对应时间轴 17:41:39–20:17:38,是全课最长的一章。它从「重载写腻了」出发,依次讲函数模板、类型推导、显式实参、按引用传模板参数、模板特化,最后进入 C++20 的 concepts。

这一章在讲什么

重载的问题很具体:max 写五个版本,函数体一模一样。模板把「逻辑」抽出来一次,让编译器按调用时用的类型生成具体函数。

沿着这条线,本章回答五个问题:

  1. 模板到底是什么?——蓝图,不是代码。
  2. 编译器怎么知道生成哪一份?——类型推导。
  3. 推导不适用时怎么办?——显式模板实参。
  4. 某个类型的默认实现不对怎么办?——模板特化。
  5. 怎样在签名里写清「这个类型必须支持什么」?——concepts。

时间轴

时间内容
17:41:39第一个函数模板:消除重载重复
17:43:45模板实例如何生成
17:47:18VS Code:trying out function templates
17:53:36用 cppinsights 查看编译器生成的实例
17:55:21只生成被调用到的实例
18:00:23类型必须支持函数体中的操作
18:05:08同一个 T 强制所有参数同类型
18:07:03用调试器观察模板实例
18:14:14反例:传指针时比较的是地址
18:19:46模板类型推导
18:22:44显式模板实参
18:35:22模板参数按引用传递
18:38:02按值与按引用两个模板重载 → 歧义
18:49:05模板特化的动机
18:52:15用 strcmp 写 const char* 特化
19:04:40concepts 简介(C++20 四大特性之一)
19:06:53标准库内置 concepts
19:08:00语法一:requires 子句
19:11:51语法二:概念写在 template<> 里
19:12:06语法三:概念写在 auto 前
19:13:02语法四:requires 放在参数列表后
19:25:36自定义 concept
19:30:38VS Code:building your own concepts
19:42:52requires 子句中的三类要求
19:44:45简单要求 vs 嵌套要求
19:45:38复合要求
19:59:32用 &&、`
20:09:37concepts 与 auto 变量

函数模板:一份蓝图

从重复的重载开始

int    maximum(int a, int b)    { return a > b ? a : b; }
double maximum(double a, double b) { return a > b ? a : b; }

两份函数体完全一样,只是类型不同。模板把类型抽成占位符:

template <typename T>
T maximum(T a, T b) {
    return a > b ? a : b;
}

T 在返回类型和两个参数里出现三次,因此这三处必须是同一个类型。

模板不是代码

这一点课件反复强调:函数模板本身不是可执行代码,编译器在看到调用时才生成真正的函数:

int x{5}, y{7};
auto r1 = maximum(x, y);          // 生成 maximum<int>
auto r2 = maximum(5.5, 78.7);     // 生成 maximum<double>

生成出来的东西叫模板实例(template instance):

// 编译器实际生成的(大致)
int maximum(int a, int b) { return a > b ? a : b; }

几个随之而来的事实:

  • 按需生成:从没调用过 maximum<std::string>,就不会生成字符串版本。
  • 只生成一次:同一个实例被多次调用,编译器复用已生成的那份。
  • 可用工具观察:cppinsights.io 会把推导后的代码显示出来;调试器的调用栈里也能看到 maximum<int> 这样的签名。

类型必须支持函数体的操作

template <typename T>
T multiply(T a, T b) { return a * b; }

multiply(5, 7);          // 生成 multiply<int>,返回 35
multiply(5.1, 7.0);      // 生成 multiply<double>,返回 35.7
multiply(std::string{"a"}, std::string{"b"});   // 编译错误

报错是 no match for 'operator*'。模板本身不检查 T 能不能相乘,直到实例化时才暴露——这就是下一节 concepts 要解决的问题。

同一个 T 的限制

maximum(4.5, 9);   // 错误:推导出 conflicting types for 'T'

4.5 推出 double,9 推出 int,两处冲突。要么把字面量写成 9.0,要么用显式模板实参(见下)。

反例:传指针

int x{5}, y{7};
int* px{&x};
int* py{&y};

auto result = maximum(px, py);   // 生成 maximum<int*>,比较的是地址

两个地址谁大取决于变量在内存里的位置,与 5、7 谁大毫无关系。课件现场演示了结果返回 5——因为 px 的地址更大。模板实例化了,但语义完全错了。

类型推导与显式模板实参

默认:从实参推导

template <typename T>
T maximum(T a, T b);

maximum(10, 23);   // T = int
maximum(5.5, 7.4); // T = double

编译器只看调用处实参的类型,没有其他线索。这是模板类型推导。

显式指定:maximum<double>(...)

auto result = maximum<double>(4, 9);   // 强制生成 double 版本

尖括号里的类型告诉编译器「用这个 T」,于是允许实参类型不同——int 会隐式转换成 double:

maximum<double>(a, d);   // a 是 int,d 是 double → 都转成 double
maximum<double>(a, e);   // e 是 std::string → 无法转成 double,编译错误

术语要分清:

术语出现位置例子
模板参数(template parameter)函数声明/定义template <typename T> 里的 T
模板实参(template argument)调用处maximum<double> 里的 double

按引用传模板参数

默认按值,对于大对象会产生拷贝。可以在模板里直接写引用:

template <typename T>
const T& maximum(const T& a, const T& b) {
    return a > b ? a : b;
}
  • 用 const T& 避免拷贝,同时禁止在函数内修改;
  • 训练器/调试器里能看到生成的实例是 const double& maximum(const double&, const double&);
  • 地址与外部相同,证明没有拷贝。

一个陷阱:同名的按值版和按引用版不能共存。

template <typename T> T maximum(T a, T b);
template <typename T> const T& maximum(const T& a, const T& b);

maximum(5.5, 7.9);   // ambiguous:两个候选都能匹配

模板特化:为某个类型换实现

默认实例对 const char* 是做错的——它比较指针。要修正,就为这个类型单独写一份:

template <>
const char* maximum<const char*>(const char* a, const char* b) {
    return (std::strcmp(a, b) > 0) ? a : b;
}

语法要点:

  • template <>:空的尖括号,表示不引入新的模板参数;
  • maximum<const char*>:显式写出特化的类型;
  • 参数列表与主模板一致。

strcmp 的返回值正好可以直接用:大于 0 表示第一个字符串更大。

const char* g{"wild"};
const char* h{"animal"};
std::cout << maximum(g, h);   // 有特化:wild;无特化:animal(比地址)

没有特化时,比较的是两个字符串字面量的地址,输出可能与字典序完全无关。

Concepts:把约束写进签名

模板的报错历来难读:错误发生在实例化处,往往牵出一长串模板内部信息。concepts 让「类型必须满足什么」变成签名的一部分,违反时直接报「约束不满足」。

标准库内置的 concepts

#include <concepts>

template <typename T>
requires std::integral<T>
T add(T a, T b) { return a + b; }
  • add(6, 7):int 是整型,通过;
  • add(11.1, 1.9):double 不满足 std::integral,编译错误,信息是 use of function 'add' with unsatisfied constraints。

常用内置 concepts:std::integral、std::floating_point、std::same_as、std::convertible_to。

四种写法

// 1. requires 子句
template <typename T>
requires std::integral<T>
T add(T a, T b);

// 2. 直接放在模板参数列表
template <std::integral T>
T add(T a, T b);

// 3. 约束 auto
std::integral auto add(std::integral auto a, std::integral auto b);

// 4. requires 放在参数列表之后
template <typename T>
T add(T a, T b) requires std::integral<T>;

四种等价,按可读性选择;语法 2 最简洁,语法 4 适合约束依赖多个参数的场合。

自定义 concept

用类型萃取定义:

template <typename T>
concept MyIntegral = std::is_integral_v<T>;

std::integral 在标准库里就是类似这样定义的。

用 requires 表达式定义:当约束不止一条,用 requires 块:

template <typename T>
concept Multipliable = requires(T a, T b) {
    a * b;        // 只检查语法是否合法
};

template <typename T>
concept Incrementable = requires(T a) {
    a += 1;
    ++a;
    a++;
};

关键点:requires 表达式只检查「这段表达式能否编译」,不检查结果的值。

简单要求、嵌套要求、复合要求

// 简单要求:只保证 size_t 相关表达式语法合法
template <typename T>
concept TinyType = requires(T t) {
    sizeof(T) < 4;        // 永远为合法语法 → 永远通过
};

// 嵌套要求:还要求表达式为真
template <typename T>
concept TinyTypeStrict = requires(T t) {
    sizeof(T) < 4;
    requires sizeof(T) < 4;
};

// 复合要求:约束表达式的返回类型
template <typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::convertible_to<int>;
};

课件的实验很能说明问题:TinyType 配上 double(8 字节)居然编译通过,因为 sizeof(T) < 4 本身是合法表达式;加上嵌套 requires 后才真正检查真假。

用逻辑运算符组合

template <typename T>
requires std::integral<T> || std::floating_point<T>
T add(T a, T b);

template <typename T>
requires std::integral<T> && TinyTypeStrict<T>
T add(T a, T b);

|| 表示「满足其一」,&& 表示「两个都要」。long long 是整型但不满足 TinyType(8 字节),正好用来验证 &&。

concepts 与 auto 变量

std::integral auto x = add(5, 8);      // 通过:返回 int
std::floating_point auto y = add(5, 8); // 失败:int 不是浮点

对字面量初始化,这种写法用处不大;但对函数返回值,它能在编译期确认「我拿到的是我想要的类型」。

本章结论

  • 函数模板是蓝图,编译器按调用生成模板实例;只生成被用到的,且同一实例只生成一次。
  • 模板体中的操作必须被实参类型支持,否则在实例化时报错。
  • 同一个 T 强制所有出现处同类型;显式模板实参 f<double>(...) 可覆盖推导并允许隐式转换。
  • 模板参数可写成 const T& 以避免拷贝;按值与按引用的同名模板不能共存。
  • 模板特化 template <> 为特定类型替换默认实现,例如用 strcmp 比较 const char*。
  • concepts 把约束写进签名,有四种等价语法,报错更清晰。
  • 自定义 concept 可用类型萃取或 requires 表达式;requires 只检查语法,要检查值需嵌套 requires。
  • concepts 可用 &&、|| 组合,也可用于 auto 变量。

理解检验

1. 为什么说「函数模板不是 C++ 代码」?这对编译产物意味着什么?

参考答案:模板只是一份带类型占位符的蓝图,本身不生成可执行代码。编译器在看到具体调用时,才用实参类型替换占位符,生成真正的函数(模板实例)并放进目标文件。因此:(a) 没被调用过的类型不会生成任何代码;(b) 同一类型只生成一次并被复用;(c) 模板中的错误往往在实例化时才暴露,而不是定义时。

2. maximum(4.5, 9) 为什么编译失败?给出两种修复方式。

参考答案:两个参数分别推导出 double 和 int,而模板只有一个 T,出现 conflicting types for 'T'。修复一:让两个实参同类型,如 maximum(4.5, 9.0);修复二:显式指定模板实参 maximum<double>(4.5, 9),此时 9 隐式转换为 double。

3. 对 const char* 调用 maximum,默认模板为什么给出错误结果?特化如何修正?

参考答案:默认实例把 T 推导为 const char*,a > b 比较的是两个指针的地址,而不是字符串内容,因此结果取决于内存布局。特化 template <> const char* maximum<const char*>(const char* a, const char* b) 改用 std::strcmp(a, b) > 0 判断字典序,从而得到正确结果。编译器看到 const char* 实参时会优先选择这个更匹配的特化。

4. 下面这个 concept 为什么拦不住 double?如何修正?

template <typename T>
concept TinyType = requires(T t) {
    sizeof(T) < 4;
};

参考答案:requires 表达式只检查花括号内语句的语法合法性。sizeof(T) < 4 对任何类型都是合法表达式(结果是 bool),所以 concept 恒为真,double 也能通过。修正方法是加一条嵌套要求,强制表达式求值为真:

template <typename T>
concept TinyType = requires(T t) {
    requires sizeof(T) < 4;
};

这样 double(8 字节)就会失败。

5. requires std::integral<T> || std::floating_point<T> 与 requires std::integral<T> && std::floating_point<T> 有什么区别?各举一个通过、一个失败的例子。

参考答案:|| 表示满足其中之一即可,&& 表示两个都必须满足。对 ||:int 通过(整型),double 通过(浮点),std::string 失败(两者都不是)。对 &&:没有任何类型能同时是整型和浮点,所以所有类型都失败——这说明 && 用于组合互补概念时通常是笔误,真正有用的是把「整型」与「尺寸约束」这类可同时满足的概念组合起来。

6. 复合要求 { a + b } -> std::convertible_to<int> 检查的是什么?

参考答案:它检查两件事:a + b 必须是合法表达式;其结果的类型必须能转换为 int。例如 char 与 char 相加结果为 int,可转换,通过;std::string 与 std::string 相加结果是 std::string,不能转换为 int,失败。注意它检查的是类型层面的可转换性,不是运行时的值。

与前后章的关系

上一章的 lambda 让函数成为值,本章的模板让「一份逻辑服务多种类型」成为可能;concepts 则为模板补上编译期的契约。下一章进入类——从「蓝图」到「用户自定义类型」,模板参数随后会与类结合,形成类模板,这也是标准库容器的基础。

This is chapter 17 of a 31-hour single video, covering 17:41:39–20:17:38 — the longest chapter in the course. It starts from overload fatigue and works through function templates, deduction, explicit arguments, reference parameters, specialization, and finally C++20 concepts.

What this chapter covers

The problem with overloading is concrete: five max versions with identical bodies. A template extracts the logic once and lets the compiler generate a concrete function per type used at the call site.

The chapter answers five questions along the way:

  1. What is a template? — a blueprint, not code.
  2. How does the compiler know which one to generate? — deduction.
  3. What if deduction is not suitable? — explicit template arguments.
  4. What if the default instance is wrong for a type? — template specialization.
  5. How do you state "this type must support X" in the signature? — concepts.

Timeline

TimeTopic
17:41:39The first function template removes overload repetition
17:43:45How template instances are generated
17:47:18In VS Code: trying out function templates
17:53:36Using cppinsights to inspect generated instances
17:55:21Only called instances are generated
18:00:23The type must support the operations in the body
18:05:08A single T forces all arguments to share a type
18:07:03Observing instances in the debugger
18:14:14Counterexample: pointer arguments compare addresses
18:19:46Template type deduction
18:22:44Explicit template arguments
18:35:22Passing template parameters by reference
18:38:02Value and reference templates overloaded → ambiguity
18:49:05Motivation for specialization
18:52:15Specializing for const char* with strcmp
19:04:40Concepts: one of the four big C++20 features
19:06:53Standard-library concepts
19:08:00Syntax 1: a requires clause
19:11:51Syntax 2: the concept inside template<>
19:12:06Syntax 3: a concept before auto
19:13:02Syntax 4: requires after the parameter list
19:25:36Writing your own concept
19:30:38In VS Code: building your own concepts
19:42:52Three kinds of requirements
19:44:45Simple versus nested requirements
19:45:38Compound requirements
19:59:32Combining concepts with && and `
20:09:37Concepts and auto variables

Function templates: one blueprint

Starting from duplicated overloads

int    maximum(int a, int b)    { return a > b ? a : b; }
double maximum(double a, double b) { return a > b ? a : b; }

The bodies are identical; only the types differ. A template makes the type a placeholder:

template <typename T>
T maximum(T a, T b) {
    return a > b ? a : b;
}

T appears in the return type and both parameters, so all three must be the same type.

A template is not code

The lecture stresses this repeatedly: a function template is not executable code. The compiler generates the real function only when it sees a call:

int x{5}, y{7};
auto r1 = maximum(x, y);          // generates maximum<int>
auto r2 = maximum(5.5, 78.7);     // generates maximum<double>

What is generated is called a template instance:

// roughly what the compiler produces
int maximum(int a, int b) { return a > b ? a : b; }

Three consequences:

  • Generated on demand: if maximum<std::string> is never called, that version never exists.
  • Generated once: repeated calls with the same type reuse the existing instance.
  • Observable: cppinsights.io displays the instantiated code, and the debugger's call stack shows signatures such as maximum<int>.

The type must support the body

template <typename T>
T multiply(T a, T b) { return a * b; }

multiply(5, 7);          // multiply<int>, returns 35
multiply(5.1, 7.0);      // multiply<double>, returns 35.7
multiply(std::string{"a"}, std::string{"b"});   // compile error

The error is no match for 'operator*'. The template itself never checks whether T can be multiplied; the failure appears only at instantiation — exactly the problem concepts address.

The single-T restriction

maximum(4.5, 9);   // error: conflicting types for 'T'

4.5 deduces double, 9 deduces int, and the two conflict. Either write 9.0, or use an explicit template argument.

Counterexample: pointers

int x{5}, y{7};
int* px{&x};
int* py{&y};

auto result = maximum(px, py);   // maximum<int*>, compares addresses

Which address is larger depends on memory layout, not on whether 5 or 7 is larger. The lecture demonstrates a return of 5 because px happens to live at a higher address. The template instantiated fine; the semantics are wrong.

Deduction and explicit template arguments

Default: deduce from the arguments

template <typename T>
T maximum(T a, T b);

maximum(10, 23);   // T = int
maximum(5.5, 7.4); // T = double

The compiler looks only at the argument types at the call. This is template type deduction.

Explicit: maximum<double>(...)

auto result = maximum<double>(4, 9);   // force the double instance

The type in angle brackets tells the compiler which T to use, which also permits differing argument types — int converts implicitly to double:

maximum<double>(a, d);   // a is int, d is double → both become double
maximum<double>(a, e);   // e is std::string → not convertible, compile error

Terminology matters:

TermAppears atExample
template parameterthe declarationT in template <typename T>
template argumentthe calldouble in maximum<double>

Passing template parameters by reference

By default parameters are by value, which copies large objects. Write the reference directly in the template:

template <typename T>
const T& maximum(const T& a, const T& b) {
    return a > b ? a : b;
}
  • const T& avoids copies and forbids modification inside;
  • The debugger shows the instance as const double& maximum(const double&, const double&);
  • Addresses match the caller's, proving no copy was made.

One trap: a by-value and a by-reference template of the same name cannot coexist.

template <typename T> T maximum(T a, T b);
template <typename T> const T& maximum(const T& a, const T& b);

maximum(5.5, 7.9);   // ambiguous: both candidates match

Specialization: a different implementation for one type

The default instance is wrong for const char* — it compares pointers. Fix it by writing a separate version for that type:

template <>
const char* maximum<const char*>(const char* a, const char* b) {
    return (std::strcmp(a, b) > 0) ? a : b;
}

Syntax points:

  • template <>: empty angle brackets, introducing no new parameter;
  • maximum<const char*>: the specialized type is written explicitly;
  • The parameter list matches the primary template.

strcmp returns a value that already fits: positive means the first string is larger.

const char* g{"wild"};
const char* h{"animal"};
std::cout << maximum(g, h);   // with specialization: wild; without: animal (address comparison)

Without the specialization, the comparison is between the addresses of two string literals and may have nothing to do with lexicographic order.

Concepts: constraints in the signature

Template diagnostics have always been painful: errors surface at instantiation and drag a pile of template internals behind them. Concepts make "what this type must satisfy" part of the signature, so violations report an unsatisfied constraint.

Standard concepts

#include <concepts>

template <typename T>
requires std::integral<T>
T add(T a, T b) { return a + b; }
  • add(6, 7): int is integral, accepted;
  • add(11.1, 1.9): double fails std::integral, compile error reading use of function 'add' with unsatisfied constraints.

Common built-ins: std::integral, std::floating_point, std::same_as, std::convertible_to.

Four syntaxes

// 1. requires clause
template <typename T>
requires std::integral<T>
T add(T a, T b);

// 2. concept in the template parameter list
template <std::integral T>
T add(T a, T b);

// 3. constrained auto
std::integral auto add(std::integral auto a, std::integral auto b);

// 4. requires after the parameter list
template <typename T>
T add(T a, T b) requires std::integral<T>;

All four are equivalent; pick by readability. Syntax 2 is the shortest, syntax 4 suits constraints depending on several parameters.

Writing your own concept

From a type trait:

template <typename T>
concept MyIntegral = std::is_integral_v<T>;

std::integral itself is defined in essentially this way.

From a requires expression — useful when the requirement is not a single trait:

template <typename T>
concept Multipliable = requires(T a, T b) {
    a * b;        // checks syntax only
};

template <typename T>
concept Incrementable = requires(T a) {
    a += 1;
    ++a;
    a++;
};

The key point: a requires expression only checks whether the expressions compile, not what they evaluate to.

Simple, nested, and compound requirements

// simple: only guarantees the expression is syntactically valid
template <typename T>
concept TinyType = requires(T t) {
    sizeof(T) < 4;        // always valid syntax → always satisfied
};

// nested: also requires the expression to be true
template <typename T>
concept TinyTypeStrict = requires(T t) {
    requires sizeof(T) < 4;
};

// compound: constrains the result type
template <typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::convertible_to<int>;
};

The lecture's experiment makes the trap concrete: TinyType accepts double (8 bytes) because sizeof(T) < 4 is a valid expression regardless of its value; adding a nested requires actually checks truth.

Combining concepts

template <typename T>
requires std::integral<T> || std::floating_point<T>
T add(T a, T b);

template <typename T>
requires std::integral<T> && TinyTypeStrict<T>
T add(T a, T b);

|| means either, && means both. long long is integral but fails TinyType (8 bytes), which is exactly how the && case is demonstrated.

Concepts and auto variables

std::integral auto x = add(5, 8);        // fine: returns int
std::floating_point auto y = add(5, 8);  // fails: int is not floating point

For literal initialization this adds little value; for a function's return value it confirms at compile time that you received the type you expected.

Takeaways

  • A function template is a blueprint; the compiler instantiates it per call, only for used types, once per type.
  • Operations in the body must be supported by the argument types, or instantiation fails.
  • A single T forces all occurrences to share a type; f<double>(...) overrides deduction and permits implicit conversions.
  • Template parameters may be const T& to avoid copies; by-value and by-reference templates of the same name cannot coexist.
  • Specialization template <> replaces the default implementation for one type, e.g. strcmp for const char*.
  • Concepts put constraints in the signature, with four equivalent syntaxes and clearer diagnostics.
  • A custom concept comes from a type trait or a requires expression; requires checks syntax, so checking values needs a nested requires.
  • Concepts combine with && and ||, and can constrain auto variables.

Comprehension check

1. Why is "a function template is not C++ code", and what does that imply for the build output?

Reference answer: a template is a blueprint with a type placeholder; it generates nothing executable by itself. Only when a concrete call appears does the compiler substitute the argument type, generate the real function (the template instance), and place it in the object file. Therefore: (a) types never used produce no code; (b) each type is instantiated once and reused; (c) errors in the template typically surface at instantiation rather than definition.

2. Why does maximum(4.5, 9) fail to compile, and what are two fixes?

Reference answer: the arguments deduce double and int, but the template has a single T, producing conflicting types for 'T'. Fix one: make the arguments the same type, e.g. maximum(4.5, 9.0). Fix two: name the template argument explicitly, maximum<double>(4.5, 9), so 9 converts implicitly to double.

3. Why does the default template give the wrong answer for const char*, and how does specialization fix it?

Reference answer: the default instance deduces T = const char*, so a > b compares pointer addresses rather than string contents, and the result depends on memory layout. The specialization template <> const char* maximum<const char*>(const char* a, const char* b) compares with std::strcmp(a, b) > 0 instead. When the arguments are const char*, the compiler prefers this more specialized definition.

4. Why does this concept fail to reject double, and how would you fix it?

template <typename T>
concept TinyType = requires(T t) {
    sizeof(T) < 4;
};

Reference answer: a requires expression checks only whether the enclosed statements are syntactically valid. sizeof(T) < 4 is a valid expression for every type (its result is bool), so the concept is always satisfied and double passes. The fix is a nested requirement that forces the expression to be true:

template <typename T>
concept TinyType = requires(T t) {
    requires sizeof(T) < 4;
};

Then double (8 bytes) fails.

5. What is the difference between requires std::integral<T> || std::floating_point<T> and the && version? Give a passing and a failing type for each.

Reference answer: || requires either constraint, && requires both. For ||: int passes (integral), double passes (floating point), std::string fails (neither). For &&: no type is both integral and floating point, so every type fails — which shows that && combining complementary concepts is usually a mistake; it is useful for combining constraints that can hold together, such as integral plus a size limit.

6. What does the compound requirement { a + b } -> std::convertible_to<int> check?

Reference answer: two things — that a + b is a valid expression, and that its result type is convertible to int. For example, char + char yields int, so it passes; std::string + std::string yields std::string, which is not convertible to int, so it fails. The check is about type-level convertibility, not runtime values.

Relation to neighbouring chapters

The previous chapter made functions into values; this chapter makes one piece of logic serve many types, and concepts add a compile-time contract to templates. The next chapter moves to classes — from blueprints to user-defined types — and template parameters will soon combine with classes to form class templates, the foundation of the standard containers.

来源