通过模板参数列表区分模板函数

7

我创建了4个类:

class C1 {};
class C2 {};
class C3 {};
class C4 {};

基于这些类的模板函数:
template<class T1, class T2>
T2 F() {
    std::cout << "F<T1, T2>" << std::endl;
    return T2();
}

template<class T>
T F();

template<>
C1 F<C1>() {
    std::cout << "F<C1>" << std::endl;
    return F<C3, C1>();
}

template<>
C2 F<C2>() {
    std::cout << "F<C2>" << std::endl;
    return F<C4, C2>();
}

这会导致编译错误:

explicit specialization 'C1 F<C1>(void)' is not a specialization of a function template
explicit specialization 'C2 F<C2>(void)' is not a specialization of a function template

然而,如果我将第一个函数F的名称更改为另一个名称F1

template<class T1, class T2>
T2 F1() {
    std::cout << "F<T1, T2>" << std::endl;
    return T2();
}

template<class T>
T F();

template<>
C1 F<C1>() {
    std::cout << "F<C1>" << std::endl;
    return F1<C3, C1>();
}

template<>
C2 F<C2>() {
    std::cout << "F<C2>" << std::endl;
    return F1<C4, C2>();
}

或者将这些函数的返回类型改为相同的(比如 int):

template<class T1, class T2>
int F() {
    std::cout << "F<T1, T2>" << std::endl;
    return 5;
}

template<class T>
int F();

template<>
int F<C1>() {
    std::cout << "F<C1>" << std::endl;
    return F<C3, C1>();
}

template<>
int F<C2>() {
    std::cout << "F<C2>" << std::endl;
    return F<C4, C2>();
}

然后编译成功,我就可以成功调用:

F<C1>();
F<C2>();

并获得预期输出:

F<C1>
F<T1, T2>
F<C2>
F<T1, T2>

问题是为什么第一个代码无法编译,但第二个和第三个代码可以编译。
https://en.cppreference.com/w/cpp/language/function_template中,我发现:

具有相同返回类型和相同参数列表的两个函数模板是不同的,并且可以通过显式模板参数列表进行区分。

我猜这就是为什么第三个代码可以编译,因为返回类型相同。但它并不能解释为什么第一个代码由于返回类型不同而无法编译。
即使是因为返回类型不能不同,也不能解释为什么第二个代码可以编译,因为F<C1>F<C2>在那里具有不同的返回类型。

你用的是哪个编译器?当我尝试原样编译你的第一个代码时,我的确没有像你所说的"... is not a specialization of ..."错误,而是在实际调用F<C1>()F<C2>()时出现了"undefined reference to ..." - Remy Lebeau
1
@RemyLebeau gcc 12.2 运行良好。然而,clang 和 msvc 却... - AndyG
1个回答

4

当你编写声明时

template<>
C1 F<C1>() { /*...*/ };

编译器必须确定你正在尝试特化哪个名为F的函数模板。这是通过模板参数推导完成的,类似于函数调用中的工作方式。特别地,没有假设模板参数列表<C1>是特化的完整模板参数列表。它可能是部分的,也可能像函数调用一样被省略。

候选模板包括:

template<class T1, class T2>
T2 F() {
    std::cout << "F<T1, T2>" << std::endl;
    return T2();
}

和模板2:

template<class T>
T F();

在函数调用中,推导会考虑调用参数和函数参数的配对,但对于显式特化匹配,则会将函数模板本身的类型与已声明特化的类型进行比较(参见[temp.deduct.decl])。

此处的特化类型为C1()。模板1的类型为T2(),而模板2的类型为T()。您在特化中明确指定了T = C1,因此没有留下什么需要推导的内容,类型相匹配。您还明确指定了T1 = C1,并且将T2()推导为C1(),得到T2 = C1

因此,两个模板都匹配:对于模板1,它匹配特化F<C1, C1>,而对于模板2,它匹配特化F<C1>

因此,现在无法确定您正在声明哪个特化。

然后考虑函数模板的部分排序以决定应优先选择模板1还是模板2作为更加特化的模板。这时,再次考虑(转换后的)模板的完整类型,见[temp.deduct.partial]/3.3。在部分排序中,显式指定的模板参数不相关,但T()可以在两个方向上针对T2()进行推导。唯一的问题是T1不能被推导到任何东西,但是如果模板参数未出现在类型中,则在部分排序中会明确忽略它(参见[temp.deduct.partial]/12)。因此,部分排序也将同时考虑两者的特化程度。

结果,你想要特化哪个函数模板是不确定的。

调用函数模板时你不会遇到相同的问题,因为返回类型在函数调用中不参与推导。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接