decltype,重载运算符的递归类型推导

时间:2015-06-14 12:57:59

标签: c++ templates c++11 language-lawyer decltype

对于带有表达式模板的类,我在重载运算符的返回类型推导期间偶然发现了以下错误。以下示例说明了错误:

template < typename T >
struct A_Number {
     T x;
};


// TAG1
template < typename T >
A_Number< T >
operator+(const A_Number< T > &a, const A_Number< T > &b)
{
    return {a.x + b.x};
}

// TAG2
template < typename T, typename S >
A_Number< T >
operator+(const A_Number< T > &a, const S &b)
{
    return {a.x + b};
}

// TAG3
template < typename T, typename S >
auto
operator+(const S &b, const A_Number< T > &a) -> decltype(a + b)
//                                                        ^^^^^
{
    return a + b;
}

int
main(void)
{
    auto x1 = A_Number< int >{1};
    auto x2 = A_Number< int >{1};

    auto res1 = x1 + 1;  // instantiates TAG2

    auto res2 = 1 + x1;  // instantiates TAG3, TAG2

    auto res3 = x1 + x2; // error, tries to match TAG3
    return EXIT_SUCCESS;
}

当尝试使用g ++ - 5或clang ++编译时,我收到此错误

fatal error: template instantiation depth exceeds maximum of 900 (use -ftemplate-depth= to increase the maximum)
 operator+(const S &b, const A_Number< T > &a) -> decltype(a + b)

显然,编译器会尝试匹配版本TAG3,但可以使用更好的匹配(TAG1)。当试图匹配时,他们试图推断出似乎导致TAG3的递归实例化的返回类型。为什么返回类型推导不能看到其他(更好匹配)重载?即使另一个重载的模板函数具有更好的匹配签名,推导返回类型也是正确的行为吗?

有趣的是,当完全省略返回类型并使用c++14进行编译时,此错误会在空气中蒸发,如下所示:

// TAG3
template < typename T, typename S >
auto
operator+(const S &b, const A_Number< T > &a) // C++14
{
    return a + b;
}

不可否认,这是一个学术问题,因为可以采用变通方法。但任何人都可以阐明这种行为是标准符合还是编译器错误?

1 个答案:

答案 0 :(得分:0)

这实际上是编译器部分的正确行为。重载分辨率有两个步骤,来自[over.match]:

  

- 首先,候选函数的一个子集(那些具有适当数量的参数并满足   选择某些其他条件)以形成一组可行的功能(13.3.2)    - 然后根据所需的隐式转换序列(13.3.3.1)选择最佳可行函数   将每个参数与每个可行函数的相应参数相匹配。

x1 + x2的候选函数是:

// TAG1
template < typename T >
A_Number< T >
operator+(const A_Number< T > &a, const A_Number< T > &b);

// TAG2
template < typename T, typename S >
A_Number< T >
operator+(const A_Number< T > &a, const S &b);

// TAG3
template < typename T, typename S >
auto operator+(const S &b, const A_Number< T > &a) -> decltype(a + b)

好的,我们需要先确定TAG3的返回类型,然后再选择最适合的候选人。现在,operator+依赖名称,因为它取决于两个模板参数。所以根据[temp.dep.res]:

  

在解析依赖名称时,会考虑以下来源的名称:
   - 在模板定义点可见的声明    - 来自名称空间的声明与函数参数的类型相关联   实例化上下文(14.6.4.1)和定义上下文。

因此,要解决TAG3的返回类型,我们需要在a+b上执行查找和重载解析。这再次给了我们相同的三个候选人(前两个找到了通常的方式,TAG3通过ADL再次找不到帮助),所以我们四处走走。

您的解决方案是:

  • 在C ++ 14中,删除TAG3的尾随返回类型。这将停止旋转木马,我们将从我们的三个可行的候选人开始,其中TAG1是最专业的,因此它将被选为最佳可行候选人。
  • 如果TAG3S,则阻止A_Number<T>被实例化。例如,我们可以为A_Number

    创建一个类型特征
    template <typename T>
    struct is_A_Number : std::false_type { };
    
    template <typename T>
    struct is_A_Number<A_Number<T>> : std::true_type { };
    
    // TAG3
    template < typename T, typename S, 
               typename = typename std::enable_if<!is_A_Number<S>::value>::type>
    auto operator+(const S &b, const A_Number< T > &a) -> decltype(a + b);
    

即使在C ++ 11中也会编译。