MSVC 2015哑元参数SFINAE实现出现重定义错误求助
你遇到的这个问题其实是MSVC 2015 Update 3对C++11 SFINAE特性支持不完善导致的典型问题——它没办法正确识别通过默认模板参数实现的SFINAE重载,会把不同条件的重载当成重复定义。
为什么会报错?
先猜一下你大概率是这么写的(这是很多人刚开始用SFINAE时的写法):
template<typename T, typename = decltype(std::declval<T>().getSize())> size_t getSize(const T& s) { return s.getSize(); } template<typename T, typename = decltype(std::declval<T>().GetLength())> size_t getSize(const T& s) { return s.GetLength(); }
这种写法在GCC、Clang里能正常工作,但MSVC 2015会认为这两个模板的签名本质相同——因为默认模板参数不会被用来区分模板重载。它看不到decltype里的SFINAE条件,只看到两个模板都接受一个T和一个默认类型参数,所以报C2572(重定义默认参数)和C2535(成员函数重定义)错误。
两种兼容MSVC 2015的解决方案
方案1:把SFINAE条件放到返回类型或函数参数中
避免用默认模板参数,转而将SFINAE逻辑放到返回类型推导里,或者用std::enable_if限定返回类型:
写法A:利用返回类型推导+逗号运算符
#include <utility> // for std::declval template<typename T> auto getSize(const T& s) -> decltype(s.getSize(), size_t{}) { return s.getSize(); } template<typename T> auto getSize(const T& s) -> decltype(s.GetLength(), size_t{}) { return s.GetLength(); } // 后续添加的length()版本 template<typename T> auto getSize(const T& s) -> decltype(s.length(), size_t{}) { return s.length(); }
这里的decltype(s.getSize(), size_t{})会先检查s.getSize()是否合法,合法的话返回size_t类型。MSVC 2015能正确识别这种返回类型不同的重载。
写法B:使用std::enable_if+类型特性
如果需要更明确的条件判断,可以先定义检测成员函数的类型特性,再用enable_if限定:
#include <type_traits> #include <utility> // 检测是否有getSize()成员函数 template<typename T, typename = void> struct has_getSize : std::false_type {}; template<typename T> struct has_getSize<T, decltype(void(std::declval<T>().getSize()))> : std::true_type {}; // 检测是否有GetLength()成员函数 template<typename T, typename = void> struct has_GetLength : std::false_type {}; template<typename T> struct has_GetLength<T, decltype(void(std::declval<T>().GetLength()))> : std::true_type {}; // 检测是否有length()成员函数 template<typename T, typename = void> struct has_length : std::false_type {}; template<typename T> struct has_length<T, decltype(void(std::declval<T>().length()))> : std::true_type {}; // 重载版本 template<typename T> typename std::enable_if<has_getSize<T>::value, size_t>::type getSize(const T& s) { return s.getSize(); } template<typename T> typename std::enable_if<has_GetLength<T>::value && !has_getSize<T>::value, size_t>::type getSize(const T& s) { return s.GetLength(); } template<typename T> typename std::enable_if<has_length<T>::value && !has_getSize<T>::value && !has_GetLength<T>::value, size_t>::type getSize(const T& s) { return s.length(); }
注意这里要加上!has_getSize<T>::value这类排除条件,确保重载不会产生歧义。
方案2:标签分发(Tag Dispatch)——更兼容旧编译器
标签分发是比SFINAE更古老也更兼容的重载方式,核心是用类型标签来分发到不同的实现函数:
#include <type_traits> #include <utility> // 先定义类型特性(和方案1里的一样) template<typename T, typename = void> struct has_getSize : std::false_type {}; template<typename T> struct has_getSize<T, decltype(void(std::declval<T>().getSize()))> : std::true_type {}; template<typename T, typename = void> struct has_GetLength : std::false_type {}; template<typename T> struct has_GetLength<T, decltype(void(std::declval<T>().GetLength()))> : std::true_type {}; template<typename T, typename = void> struct has_length : std::false_type {}; template<typename T> struct has_length<T, decltype(void(std::declval<T>().length()))> : std::true_type {}; // 内部实现函数 namespace detail { // 优先处理getSize() template<typename T> size_t getSizeImpl(const T& s, std::true_type /* has_getSize */) { return s.getSize(); } // 没有getSize()时,检查GetLength() template<typename T> size_t getSizeImpl(const T& s, std::false_type /* has_getSize */) { return getSizeImpl(s, std::integral_constant<bool, has_GetLength<T>::value>{}); } // 处理GetLength() template<typename T> size_t getSizeImpl(const T& s, std::true_type /* has_GetLength */) { return s.GetLength(); } // 没有GetLength()时,处理length() template<typename T> size_t getSizeImpl(const T& s, std::false_type /* has_GetLength */) { return s.length(); } } // 对外接口 template<typename T> size_t getSize(const T& s) { return detail::getSizeImpl(s, has_getSize<T>{}); }
这种写法MSVC 2015完全能正确处理,而且逻辑清晰,后续添加新的成员函数版本也很方便。
总结
MSVC 2015对SFINAE的支持确实有缺陷,尤其是默认模板参数的场景。你可以选择上面两种方案中的任意一种来修复问题——如果追求简洁用方案1的返回类型推导写法,如果追求兼容性和扩展性,标签分发会更稳妥。
内容的提问来源于stack exchange,提问作者Victor Istomin

