为什么SFINAE在默认函数参数中没有在右侧工作?

时间:2014-07-23 11:46:03

标签: c++ sfinae

我有这段代码:

struct My
{
   typedef int foo;
};

struct My2
{
};


template <typename T>
void Bar(const T&, int z = typename T::foo())
{
    std::cout << "My" << std::endl; 
}


void Bar(...)
{
    std::cout << "..." << std::endl; 
}

int main() 
{
    My my;
    Bar(my); // OK
    My2 my2;
    Bar(my2); // Compile error: no type named ‘foo’ in ‘struct My2’
    return 0;
}

我想,如果某个类T内部没有typedef foo,编译器应该排除第一个重载并选择带省略号的重载。但是我在MSVC,gcc和clang上查看了这段代码,我在这些编译器上遇到了编译错误。为什么SFINAE在这种情况下不起作用?

3 个答案:

答案 0 :(得分:10)

z的类型不受模板替换的影响,始终为int。这意味着SFINAE没有机会,而在尝试解析T::foo默认值时会出现编译器错误。默认参数不参与重载解析,而是仅在函数调用中丢失时才实例化。该标准的第14.7.1节(第13/14段)描述了这种行为,但并没有说明这里缺乏SFINAE的原因。

可以通过将z的类型设为模板参数来允许SFINAE发生,如下所示:

(实例:http://ideone.com/JynMye

#include <iostream>

struct My
{
   typedef int foo;
};

struct My2
{
};

template<typename T, typename I=typename T::foo> void Bar(const T&, I z = I())
{
    std::cout << "My\n";
}

void Bar(...)
{
    std::cout << "...\n";
}

int main() 
{
    My my;
    Bar(my); // OK
    My2 my2;
    Bar(my2); // Also OK
    return 0;
}

这将使用&#34;我的&#34;第一个电话的版本,&#34; ...&#34;第二个电话的版本。输出是

My
...

但是,如果无效Bar(...)是模板,无论出于何种原因,&#34;我的&#34;版本永远不会有机会:

(实例:http://ideone.com/xBQiIh

#include <iostream>

struct My
{
   typedef int foo;
};

struct My2
{
};

template<typename T, typename I=typename T::foo> void Bar(const T&, I z = I())
{
    std::cout << "My\n";
}

template<typename T> void Bar(T&)
{
    std::cout << "...\n";
}

int main() 
{
    My my;
    Bar(my); // OK
    My2 my2;
    Bar(my2); // Also OK
    return 0;
}

在这里,&#34; ...&#34;在这两种情况下都会调用version。输出是:

...
...

一种解决方案是使用类模板(部分)专业化;提供&#34; ...&#34; version作为基础,第二个参数的类型默认为int,&#34;我的&#34;版本作为专门化,其中第二个参数是typename T::foo。结合普通的模板函数来推导出T并发送给相应的类&#39;成员函数,这会产生预期的效果:

(实例:http://ideone.com/FanLPc

#include <iostream>

struct My
{
   typedef int foo;
};

struct My2
{
};

template<typename T, typename I=int> struct call_traits {
    static void Bar(...)
    {
        std::cout << "...\n";
    }
};

template<typename T> struct call_traits<T, typename T::foo> {
    static void Bar(const T&, int z=typename T::foo())
    {
        std::cout << "My\n";
    }
};

template<typename T> void Bar(const T& t)
{
    call_traits<T>::Bar(t);
}

int main() 
{
    My my;
    Bar(my); // OK
    My2 my2;
    Bar(my2); // Still OK
    return 0;
}

此处输出为:

My
...

答案 1 :(得分:4)

类型zint,编译器不会推断,没有SFINAE的空间。用于初始化z的值基于默认值T::foo,该值不存在;因此错误。

如果z的类型被提升为模板本身,则替换现在可能会失败,并且SFINAE会启动。

#include <iostream>

struct My
{
   typedef int foo;
};

struct My2
{
};

template <typename T, typename I = typename T::foo>
void Bar(const T&, I z = I())
{
    (void)z; // silence any warnings on unused
    std::cout << "My" << std::endl; 
}

void Bar(...)
{
    std::cout << "..." << std::endl; 
}

int main() 
{
    My my;
    Bar(my);
    My2 my2;
    Bar(my2); // Compiles
    return 0;
}

Live sample

为了使函数模板成为候选函数的重载列表的一部分,模板参数推导必须成功。如果失败,则候选人将从列表中删除。因此,如果没有发生扣减失败,则会将其添加到候选列表中(但如果最终选择了此错误,则不会排除其他错误。)

  

14.8.3 / 1重载分辨率

     

函数模板可以通过其名称的(非模板)函数或同名的(其他)函数模板重载。当写入对该名称的调用(显式或隐式使用运算符表示法)时,将为每个函数模板执行模板参数推导(14.8.2)和检查任何显式模板参数(14.3)以查找模板参数值( if if)可以与该函数模板一起使用来实例化可以使用调用参数调用的函数模板特化。对于每个函数模板,如果参数推导和检查成功,则使用模板参数(推导和/或显式)来合成单个函数模板特化的声明,该声明被添加到用于重载解析的候选函数集中。如果对于给定的函数模板,参数推导失败,则不会将该函数添加到该模板的候选函数集。完整的候选函数集包括所有合成的声明和所有同名的非模板重载函数。除非在13.3.3中明确指出,否则合成声明将被视为重载解析的其余部分中的任何其他函数。

模板参数推导是对函数类型及其模板参数本身执行的。

  

14.8.2 / 8模板参数推断

     

如果替换导致无效的类型或表达式,则类型推导失败。如果使用替换参数写入,则无效的类型或表达式将是格式错误的,需要诊断。 [注意:如果不需要诊断,程序仍然是不正确的。访问检查是替换过程的一部分。 -end note]只有函数类型的直接上下文及其模板参数类型中的无效类型和表达式才会导致演绎失败。

从OP中,函数Bar<T>被添加到候选列表中,因为可以推断出T的类型是什么。它被实例化并且检查了默认参数,因此它失败了。

  

14.7.1 / 13隐式实例化

     

如果以需要使用默认参数的方式调用函数模板f,则查找从属名称,检查语义约束,并且默认参数中使用的任何模板的实例化为完成,好像默认参数是在具有相同范围的函数模板特化中使用的初始化程序,相同的模板参数以及与该点使用的函数模板f相同的访问权限。此分析称为默认参数实例化。然后将实例化的默认参数用作f的参数。

摘自draft n3797

答案 2 :(得分:2)

另外一个C ++ 03兼容选项。因为在上面的答案中,默认参数在模板函数中使用,并且在标准中不允许。

#include <iostream>

struct TypeWithFoo{
   typedef int Foo;
};

template<typename T, bool>
struct onFooAction;

template<typename T>
struct onFooAction<T, false>{
   void operator ()(const T &t){
        std::cout << "No foo :(\n";
   }
};

template<typename T>
struct onFooAction<T, true>{
   void operator ()(const T &t){
      std::cout << "Foo =)\n";
   }
};

template<typename T>
struct hasFoo{
   typedef char yes[1];
   typedef char no[2];

   template<typename C>
   static yes& testForFoo(typename C::Foo*);

   template<typename>
   static no& testForFoo(...);

   static const bool value = sizeof(testForFoo<T>(0)) == sizeof(yes);
};

template<typename T>
void bar(const T &t){
   onFooAction<T, hasFoo<T>::value>()(t);
}

int main(){
  bar(10);
  bar(TypeWithFoo());
}