13

在我的库中尝试为 const 和非 const 模板参数提供函数时,我遇到了一个奇怪的问题。以下源代码是一个最小的示例现象:

#include <iostream>


template<typename some_type>
struct some_meta_class;

template<>
struct some_meta_class<int>
{
    typedef void type;
};



template<typename some_type>
struct return_type
{
    typedef typename some_meta_class< some_type >::type test;

    typedef void type;
};



template<typename type>
typename return_type<type>::type foo( type & in )
{
    std::cout << "non-const" << std::endl;
}

template<typename type>
void foo( type const & in )
{
    std::cout << "const" << std::endl;
}


int main()
{
    int i;

    int const & ciref = i;
    foo(ciref);
}

我试图为 foo 实现一个非 const 版本和一个 const 版本,但不幸的是,这段代码无法在 CLANG 3.0 和 gcc 4.6.3 上编译。

main.cpp:18:22:错误:未定义模板“some_meta_class”的隐式实例化

因此,出于某种原因,编译器希望将非 const 版本的 foo 用于 const int 引用。这显然会导致上面的错误,因为 some_meta_class 没有实现。奇怪的是,如果您进行以下更改之一,代码编译得很好并且可以工作:

  • 取消注释/删除非常量版本
  • 取消注释/删除 return_type::test 的 typedef

这个例子当然是简约和纯粹的学术。在我的库中,我遇到了这个问题,因为 const 和非 const 版本返回不同的类型。我通过使用部分专业化的辅助类来解决这个问题。

但是为什么上面的例子会导致这种奇怪的行为呢?为什么编译器不想使用 const 版本有效且匹配更好的非 const 版本?

4

1 回答 1

24

原因是执行函数调用解析的方式,以及模板参数推导和替换。

  1. 首先,执行名称查找。这为您提供了两个具有匹配名称的函数foo()

  2. 其次,执行类型推导:对于每个具有匹配名称的模板函数,编译器尝试推导函数模板参数,这将产生可行的匹配。你得到的错误发生在这个阶段。

  3. 第三,重载决议进入游戏。这仅执行了类型推导并且确定了用于解析调用的可行函数的签名之后,这是有道理的:编译器只有在找到所有候选者的确切签名后才能有意义地解析您的函数调用。

您收到与非常量重载相关的错误并不是因为编译器选择它作为解决调用的最可行候选者(这将是第 3 步),而是因为编译器在实例化其返回类型时产生错误在步骤 2 中确定其签名。

但是,为什么这会导致错误并不完全清楚,因为人们可能会期望SFINAE适用(替换失败不是错误)。为了澄清这一点,我们可以考虑一个更简单的例子:

template<typename T> struct X { };

template<typename T> typename X<T>::type f(T&) { }  // 1
template<typename T> void f(T const&) { }           // 2

int main()
{
    int const i = 0;
    f(i); // Selects overload 2
}

在本例中,SFINAE 适用:在第 2 步中,编译器将为T上述两个重载中的每一个进行推断,并尝试确定它们的签名。在重载 1 的情况下,这会导致替换失败X<const int>未定义任何type(no typedefin X)。但是,由于 SFINAE,编译器简单地将其丢弃,并发现重载 2 是可行的匹配项。因此,它选择了它。

现在让我们以反映您的示例的方式稍微更改示例:

template<typename T> struct X { };

template<typename Y>
struct R { typedef typename X<Y>::type type; };

// Notice the small change from X<T> into R<T>!
template<typename T> typename R<T>::type f(T&) { }  // 1
template<typename T> void f(T const&) { }           // 2

int main()
{
    int const i = 0;
    f(i); // ERROR! Cannot instantiate R<int const>
}

改变的是,重载 1 不再返回X<T>::type,而是返回R<T>::type。这又与中的声明相同,因此人们可能期望它产生相同的结果。但是,在这种情况下,您会收到编译错误。为什么?X<T>::typetypedefR

该标准有答案(第 14.8.3/8 段):

如果替换导致无效的类型或表达式,则类型推导失败。无效类型或表达式是如果使用替换参数编写的格式错误的类型或表达式。[...] 只有在函数类型及其模板参数类型的直接上下文中的无效类型和表达式才会导致推导失败。

显然,第二个示例(以及您的示例)在嵌套上下文中生成错误,因此 SFINAE 不适用。我相信这回答了你的问题。

顺便说一句,有趣的是,自 C++03 以来,这种情况发生了变化,更一般地说(第 14.8.2/2 段):

[...]如果模板参数或函数模板的函数类型中的替换导致类型无效,则类型推导失败。[...]

如果您对事情发生变化的原因感到好奇,本文可能会给您一个想法。

于 2013-01-31T14:37:17.767 回答