【问题标题】:SFINAE: Compiler doesn't pick the specialized template classSFINAE:编译器不选择专门的模板类
【发布时间】:2012-07-04 00:13:45
【问题描述】:

我有一个SFINAE 问题:

在下面的代码中,我希望 C++ 编译器选择专用函子并打印“special”,但它打印的是“general”。

#include <iostream>
#include <vector>

template<class T, class V = void>
struct Functor {
  void operator()() const {
    std::cerr << "general" << std::endl;
  }
};

template<class T>
struct Functor<T, typename T::Vec> {
  void operator()() const {
    std::cerr << "special" << std::endl;
  }
};

struct Foo {
  typedef std::vector<int> Vec;
};

int main() {
  Functor<Foo> ac;
  ac();
}

如何修复它以便自动使用专用结构?请注意,我不想直接将 Functor 结构体专门用于 Foo,但我想将其专门用于具有 Vec 类型的所有类型。

P.S.:我使用的是 g++ 4.4.4

【问题讨论】:

  • 去掉了compiler标签,一般用来问编译过程本身,而这个问题是关于C++语言的。

标签: c++ templates sfinae


【解决方案1】:

很抱歉在最后一个答案中误导了您,我想了一会儿会更简单。所以我会尝试在这里提供一个完整的解决方案。解决此类问题的一般方法是编写一个 traits 辅助模板,并将其与enable_if(C++11、boost 或手动实现)一起使用来决定类专业化:

特质

一种简单的方法,不一定是最好的,但写起来很简单:

template <typename T>
struct has_nested_Vec {
    typedef char yes;
    typedef char (&no)[2];
    template <typename U>
    static yes test( typename U::Vec* p );
    template <typename U>
    static no test( ... );

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

方法很简单,提供两个模板函数,返回不同大小的类型。其中一个采用嵌套的Vec 类型,另一个采用省略号。对于所有具有嵌套Vec 的类型,第一个重载是更好的匹配(省略号是任何类型的最差匹配)。对于那些没有嵌套 Vec 的类型,SFINAE 将丢弃该重载,剩下的唯一选项将是省略号。所以现在我们有一个 trait 来询问任何类型是否有嵌套的 Vec 类型。

如果

启用

你可以从任何库中使用它,或者你可以自己滚动,这很简单:

template <bool state, typename T = void>
struct enable_if {};

template <typename T>
struct enable_if<true,T> {
    typedef T type;
};

当第一个参数是false时,基本模板是唯一的选项,并且没有嵌套的type,如果条件是true,那么enable_if有一个嵌套的type我们可以与 SFINAE 一起使用。

实施

现在我们需要提供模板和专门化,以便仅将 SFINAE 用于具有嵌套 Vec 的那些类型:

template<class T, class V = void>
struct Functor {
    void operator()() const {
        std::cerr << "general" << std::endl;
    }
};
template<class T>
struct Functor<T, typename enable_if<has_nested_Vec<T>::value>::type > {
    void operator()() const {
        std::cerr << "special" << std::endl;
    }
};

每当我们用一个类型实例化Functor 时,编译器将尝试使用特化,这将反过来实例化has_nested_Vec 并获得一个真值,传递给enable_if。对于值为false 的类型,enable_if 没有嵌套的type 类型,因此在 SFINAE 中将丢弃特化并使用基本模板。

您的特殊情况

在您的特定情况下,您似乎不需要专门化整个类型而只需要专门化运算符,您可以将三个元素混合为一个元素:一个 Functor 调度到两个内部元素之一基于 Vec 存在的模板化函数,无需 enable_if 和特征类:

template <typename T>
class Functor {
   template <typename U>
   void op_impl( typename U::Vec* p ) const {
      std::cout << "specialized";
   }
   template <typename U>
   void op_impl( ... ) const {
      std::cout << "general";
   }
public:
   void operator()() const {
      op_impl<T>(0);
   }
};

【讨论】:

    【解决方案2】:

    尽管这是一个老问题,但我认为仍然值得提供更多替代方案来快速修复原始代码。

    基本上,问题不在于使用 SFINAE(实际上那部分很好),而在于主模板中的默认参数 (void) 与部分特化中提供的参数 (@ 987654322@)。由于主模板中的默认参数,Functor&lt;Foo&gt; 实际上表示Functor&lt;Foo, void&gt;。当编译器尝试使用特化来实例化它时,它会尝试将两个参数与特化中的参数进行匹配并失败,因为void 不能替代std::vector&lt;int&gt;。然后它回退到使用主模板进行实例化。

    因此,假设您的所有Vecs 都是std::vector&lt;int&gt;s,最快的解决方法是替换该行

    template<class T, class V = void>
    

    有了这个

    template<class T, class E = std::vector<int>>
    

    现在将使用特化,因为参数将匹配。简单,但过于局限。显然,我们需要更好地控制特化中参数的类型,以使其与我们可以在主模板中指定为默认参数的内容相匹配。一种不需要定义新特征的快速解决方案是:

    #include <iostream>
    #include <vector>
    #include <type_traits>
    
    template<class T, class E = std::true_type>
    struct Functor {
      void operator()() const {
        std::cerr << "general" << std::endl;
      }
    };
    
    template<class T>
    struct Functor<T, typename std::is_reference<typename T::Vec&>::type> {
      void operator()() const {
        std::cerr << "special" << std::endl;
      }
    };
    
    struct Foo {
      typedef std::vector<int> Vec;
    };
    
    int main() {
      Functor<Foo> ac;
      ac();
    }
    

    这适用于此处可能有意义的任何 Vec 类型,包括基本类型和数组,以及指向它们的引用或指针。

    【讨论】:

      【解决方案3】:

      检测成员类型是否存在的另一种方法是使用void_t。由于有效的部分特化比一般实现更可取,只要它们与默认参数匹配,我们想要一个在有效时评估为void 的类型,并且仅在指定成员存在时才有效;这种类型通常(并且,从 C++17 开始,是规范的)称为void_t。

      template<class...>
      using void_t = void;
      

      如果您的编译器不能正确支持它(在早期的 C++14 编译器中,别名模板中未使用的参数不能保证确保 SFINAE,违反上述void_t),可以使用解决方法。

      template<typename... Ts> struct make_void { typedef void type; };
      template<typename... Ts> using void_t = typename make_void<Ts...>::type;
      

      从 C++17 开始,void_t 在实用程序库中可用,位于 type_traits。

      #include <iostream>
      #include <vector>
      #include <type_traits> // For void_t.
      
      template<class T, class V = void>
      struct Functor {
        void operator()() const {
          std::cerr << "general" << std::endl;
        }
      };
      
      // Use void_t here.
      template<class T>
      struct Functor<T, std::void_t<typename T::Vec>> {
        void operator()() const {
          std::cerr << "special" << std::endl;
        }
      };
      
      struct Foo {
        typedef std::vector<int> Vec;
      };
      
      int main() {
        Functor<Foo> ac;
        ac();
      }
      

      这样,输出是special,正如预期的那样。


      在这种情况下,由于我们要检查成员类型是否存在,所以过程非常简单;它可以在没有表达式 SFINAE 或 type_traits 库的情况下完成,允许我们在必要时重写检查以使用 C++03 工具。

      // void_t:
      // Place above Functor's definition.
      template<typename T> struct void_t { typedef void type; };
      
      // ...
      
      template<class T>
      struct Functor<T, typename void_t<typename T::Vec>::type> {
        void operator()() const {
          std::cerr << "special" << std::endl;
        }
      };
      

      据我所知,这应该适用于大多数(如果不是全部)支持 SFINAE 的 C++03、C++11、C++14 或 C++1z 兼容的编译器。这在处理稍微落后于标准的编译器时,或者在为尚无 C++11 兼容编译器的平台进行编译时非常有用。


      有关void_t 的更多信息,请参阅cppreference。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-08-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多