PAYATHON 2026

如何让 template 找到 concept-constrained template 的函数定义?

排障赵师傅

结论

要让 foo 正确调用受 concept 约束的 bar,这个重载的声明必须在 foo 定义之前可见,所选模板的定义也必须可用于实例化。最直接的做法是把受约束的 bar 定义放进头文件,写在 foo 前面。

原代码有两个问题:

  • 无约束的 bar 声明和受 std::integral 约束的 bar 定义是两个不同的函数模板。
  • foo<int> 中的调用无法通过普通名称查找找到后面才声明的重载。

取消 bar(5) 的注释后,即使链接成功,这两个问题也依然存在,不能依赖这种行为。

为什么已有定义,仍然找不到 bar

头文件中的声明是:

template <typename T>
void bar(T value);

源文件中的定义是:

template <std::integral T>
void bar(T value)
{
    std::println("bar(integral[{}])", value);
}

后者增加了约束,形成了另一个重载。它没有补全前面的声明,也不是前一个模板的特化。判断函数模板是否为同一个实体,不能只看替换参数后是否都得到 void(int),还要考虑约束是否满足相关声明的等价性要求。C++ 工作草案:函数模板重载

foo 的定义是:

template <typename T>
void foo(T value)
{
    bar<T>(value);
}

这个调用依赖模板参数,遵循两阶段名称查找规则:

  1. 普通名称查找考虑 foo 定义处可见的声明,此时只有无约束的 bar。
  2. 实例化时可以通过 ADL(实参依赖查找)补充候选,但不会重新搜索调用位置之前的全部全局重载。

Microsoft 的两阶段名称查找文档给出了类似示例:在模板定义之后才出现、又无法通过 ADL 找到的重载,不会参与该调用的重载决议。Microsoft:两阶段名称查找

调用 foo(6) 时,实参类型是 int。基本类型 int 没有关联命名空间,因此 ADL 无法找到源文件中后声明的 bar。C++ 工作草案:ADL

因此,foo<int> 引用的是无约束模板的 bar<int>。程序没有提供这个模板的定义,便会出现未解析的外部符号。仅把调用改成 bar(value),也无法解决声明顺序的问题。

为什么加上 bar(5) 就能运行

main 直接调用 bar(5) 时,两个重载都已可见。对于 int,重载决议会选中受 std::integral 约束的版本,这次调用会触发它的隐式实例化。

bar(5) 触发的是隐式实例化,显式实例化则使用这样的语法:

template void bar<int>(int);

直接调用不会改变 foo<int> 内部的名称查找结果。按标准规则,那里引用的仍然是无约束模板的实例。

题述的链接成功现象,符合两个不同模板的实例生成了相同链接符号的情况:直接调用使受约束版本生成了函数体,链接器随后用它解析了原本缺失的引用。这是从实现层面给出的一种解释。要确认具体构建中的原因,需要检查目标文件的修饰名,仅凭输出还不能确定。

因此,额外调用一次 bar,或者直接给原代码添加显式实例化,都不能作为通用修复。应先修正声明与查找关系。

推荐实现:把受约束重载放在 foo 前面

删除原来只有声明的无约束版本,把需要的重载定义放入头文件:

// my.hpp
#pragma once

#include <concepts>
#include <print>

template <std::integral T>
void bar(T value)
{
    std::println("bar(integral[{}])", value);
}

template <std::floating_point T>
void bar(T value)
{
    std::println("bar(floating_point[{}])", value);
}

template <typename T>
void foo(T value)
{
    bar(value);
}
// my.cpp
#include "my.hpp"

int main()
{
    foo(6);
    foo(6.5);
}

输出应为:

bar(integral[6])
bar(floating_point[6.5])

这里使用 bar(value),由实参推导模板参数,也便于支持普通函数重载。如果只考虑函数模板,保留 bar<T>(value) 也可以。修复依赖的是声明在 foo 定义处可见。

使用时还需注意:

  • 使用 std::integral 和 std::floating_point 时,应直接包含 <concepts>,不要依赖其他头文件间接引入。
  • concepts 属于 C++20;示例中的 std::println 需要 C++23 及相应的标准库支持。
  • 在普通头文件使用方式下,若希望任意受支持类型都能自动实例化,通常需要让模板定义随头文件可见。也可以把实现放进 .tpp 等文件,再由头文件包含。
  • 对于不支持的类型,可以不提供兜底重载,让调用在编译期报错。不要留下一个没有定义的通用声明,否则错误可能拖到链接阶段。

根据扩展需求选择模式

类型分组由库统一维护

使用上面的受约束重载即可。先声明所有应参与普通查找的重载,再定义 foo。

如果各类型分支都由同一处代码维护,也可以在一个模板的函数体中使用 if constexpr,按 concept 分支处理,省去维护多个重载声明顺序的工作。

允许用户为自己的类型提供实现

可以在自定义类型所在的命名空间中定义 bar,让 foo 通过非限定调用使用 ADL。

例如,在前述头文件之外增加:

namespace app {

struct Value {};

void bar(Value)
{
    // 对 app::Value 执行自定义操作
}

} // namespace app

int main()
{
    foo(app::Value{});
}

app::Value 的关联命名空间是 app,因此 ADL 可以找到 app::bar。这里应保留 bar(value);写成 bar<T>(value) 会排除这个普通函数重载,写成 ::bar(value) 则会禁止 ADL。

这种扩展方式适用于具有关联命名空间的自定义类型,无法让 int 自动关联到后来添加的全局重载。

必须把实现留在 .cpp

如果支持的类型集合固定,可以在头文件中声明正确的受约束模板,并在实现文件中显式实例化:

// my.hpp
#pragma once

#include <concepts>

template <std::integral T>
void bar(T value);

extern template void bar<int>(int);

template <typename T>
void foo(T value)
{
    bar(value);
}
// my.cpp
#include "my.hpp"
#include <print>

template <std::integral T>
void bar(T value)
{
    std::println("bar(integral[{}])", value);
}

template void bar<int>(int);

int main()
{
    foo(6);
}

这里的声明和定义使用相同的约束,显式实例化提供了 foo<int> 所需的函数。extern template 是显式实例化声明,函数定义之后的 template void bar<int>(int); 是显式实例化定义。C++ 工作草案:显式实例化

这种写法需要为每种受支持的类型分别提供实例。若希望 long、unsigned 等类型也能随调用自动获得实现,把定义放在头文件中更合适。

备注:内容仅供参考。