C++ / a working model

51 / 80   ·   C++11   ·   约 10 分钟

模板推导:先匹配参数,再实例化代码

先记住这句话

模板是一组由参数生成的声明与实现,不是把任意实参强行转换成同一种类型。理解按值与按引用推导、非类型参数以及定义可见性,能更快解释调用失败、数组退化和依赖名称错误。

本篇内容
  1. 推导不等于普通参数转换
  2. 实例化点需要看到什么
  3. 让接口暴露真正约束
  4. 运行示例
  5. 动手练习

推导不等于普通参数转换

函数模板调用先根据形参与实参匹配模板参数,再形成可参与重载选择的候选。对于 template<class T> T larger(T, T),传入 int 与 double 会给 T 提供冲突信息,通常不能像普通 double 参数那样先转换再完成推导。可以显式指定 T,或把接口设计为两个类型参数。

按值参数会忽略实参顶层 const,并让数组与函数参与相应退化;按引用参数能保留数组边界与 const。示例的数组引用参数 const T (&)[N] 同时推导元素类型与长度,N 是编译期数值参数,不需要另外传入一个可能错误的运行期长度。

实例化点需要看到什么

模板定义通常放在头文件,是为了让使用它的翻译单元能在需要时完成实例化,并不是语言要求所有模板必须写在 .h。封闭类型集合也可通过显式实例化安排独立编译,但这会限制支持的类型,需要统一维护实例化清单。

依赖模板参数的名称不能总在第一次解析时确定。例如 typename C::value_type 明确告诉编译器它是类型;依赖对象上的成员模板有时还需要 template 消歧。它们是语法信息,不会产生运行期开销,也不是为了让编译器“更努力猜测”。

让接口暴露真正约束

示例的 larger 按值返回选中的对象,避免把临时实参的引用返回给调用者;数组长度函数则只读取类型信息,不接触元素内容。类型断言与数值断言分别验证推导结果与业务结果,这两类问题不应混为一谈。

模板能实例化不代表任何输入都符合业务语义。比较函数仍需要合理的排序关系,算术仍可能溢出。C++20 concepts 能把部分类型条件提前写到接口上,但不能证明所有运行期性质。遇到长诊断时,应先找最早违反的参数要求,而不是从最后一层标准库报错开始修改。

容易答错的地方

  • 同一 T 从多个参数推导时必须一致,不能指望推导阶段自动寻找大家都能转换到的共同类型。
  • 返回 const T& 虽能减少一次复制,却可能把临时实参的引用交给调用者;泛型返回策略必须明确生命期。

运行一个例子

最低标准 C++11 · 完整程序 · 下载 .cpp

#include <cassert>
#include <cstddef>
#include <iostream>
#include <type_traits>

template<class T>
T larger(T a, T b) { return a < b ? b : a; }
template<class T, std::size_t N>
constexpr std::size_t extent(const T (&)[N]) { return N; }

int main() {
    const int values[] = {2, 4, 6};
    static_assert(extent(values) == 3, "array extent survives");
    auto result = larger<double>(2, 3.5);
    static_assert(std::is_same<decltype(result), double>::value, "explicit type");
    assert(result == 3.5);
    assert(larger(2, 4) == 4);
    std::cout << extent(values) << ' ' << larger(2, 4) << '\n';
}

在本地编译

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread modern-templates.cpp -o example && ./example

预期结果

3 4

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

如果将 extent 的参数改为 const T*,还能从数组调用中推导 N 吗?为什么原版本不接受普通 int*?

查看参考答案

不能。数组退化成指针后,指针类型中没有元素数量;若仍保留模板参数 N,它将无从推导。原版本要求真正的数组引用,普通 int* 没有可匹配的数组边界。运行期连续区域应使用指针加长度,或在 C++20 使用 span。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录