基于模板模板参数的SFINAE检测类模板成员函数问题
需求是设计一个traits,判断类是否拥有指定的模板非静态成员函数(可扩展至模板静态成员函数)。针对非模板成员函数已有成熟实现,但常用的模板成员函数检测方案是检查特定实例化版本,存在不足:比如检测int实例时,仅接受浮点型的成员函数会被判定为不存在,不符合预期。
为避免使用非通用的“测试类型”,尝试基于模板模板参数实现SFINAE,编写了如下代码,但出现多处问题:
#include <cassert> #include <concepts> #include <type_traits> struct ArbitraryClass { template <std::floating_point T> void Serialize(T&) {} }; struct OtherClass {}; template <typename... T> struct void_T {}; template <typename Class> struct wrapper { template <typename T, typename U = decltype(std::declval<Class>().template Serialize<T>( std::declval<T&>()))> struct type_impl {}; template <typename T> struct type : type_impl<T> {}; }; template <typename T, template <typename U> typename = void_T> struct HasSerialize : std::false_type {}; template <typename T> struct HasSerialize<T, wrapper<T>::template type> : std::true_type {}; int main() { static_assert(HasSerialize<ArbitraryClass>::value, "Has no Serialize"); static_assert(not HasSerialize<OtherClass>::value, "Has no Serialize"); static_assert(not HasSerialize<int>::value, "Has no Serialize"); }
所有测试编译器均导致第一个断言意外失败;clang还拒绝编译另外两个断言,报错信息如下:
<source>:17:68: error: no member named 'Serialize' in 'OtherClass' 17 | typename U = decltype(std::declval<Class>().template Serialize<T>( | ~~~~~~~~~~~~~~~~~~~~~ ^ <source>:27:24: note: in instantiation of template class 'wrapper<OtherClass>' requested here 27 | struct HasSerialize<T, wrapper<T>::template type> : std::true_type {}; | ^ <source>:31:23: note: during template argument deduction for class template partial specialization 'HasSerialize<T, wrapper<T>::template type>' [with T = OtherClass] 31 | static_assert(not HasSerialize<OtherClass>::value, "Has no Serialize"); | ^ <source>:31:23: note: in instantiation of template class 'HasSerialize<OtherClass>' requested here <source>:17:58: error: member reference base type 'int' is not a structure or union 17 | typename U = decltype(std::declval<Class>().template Serialize<T>( | ~~~~~~~~~~~~~~~~~~~~~^ ~~~~~~~~~ <source>:27:24: note: in instantiation of template class 'wrapper<int>' requested here 27 | struct HasSerialize<T, wrapper<T>::template type> : std::true_type {}; | ^ <source>:32:23: note: during template argument deduction for class template partial specialization 'HasSerialize<T, wrapper<T>::template type>' [with T = int] 32 | static_assert(not HasSerialize<int>::value, "Has no Serialize"); | ^ <source>:32:23: note: in instantiation of template class 'HasSerialize<int>' requested here
用户原本思路是:HasSerialize<ArbitraryClass>可选用第二个模板参数为void_T或更特化的wrapper<T>::template type,因此应继承std::true_type;而其他类的wrapper<T>::template type因无法替换第二个模板参数而无效。但实际运行不符合预期,以下是错误原因分析及可行实现方案:
错误原因分析
第一个断言失败的核心原因:
特化版本HasSerialize<T, wrapper<T>::template type>中,wrapper<T>::template type是仅接受单个模板参数的模板,而主模板的默认参数void_T是接受任意数量模板参数的模板。两者的模板参数数量不匹配,导致特化版本无法匹配主模板,编译器只会选择主模板的std::false_type,因此第一个断言失败。Clang编译报错的原因:
在进行类模板特化的参数推导时,编译器会实例化wrapper<T>的完整定义,而非仅检查type模板是否存在。当T是OtherClass或int时,wrapper<T>中的type_impl会尝试实例化declval<Class>().template Serialize<T>,这会直接触发硬编译错误(OtherClass无Serialize成员、int不是类类型),而SFINAE仅适用于模板参数推导阶段的替换失败,无法处理这种实例化错误。
可行的实现方案
方案1:使用C++20概念
利用C++20的概念特性,可以更直观地验证模板成员函数的存在性:
#include <concepts> #include <type_traits> struct ArbitraryClass { template <std::floating_point T> void Serialize(T&) {} }; struct OtherClass {}; template <typename C> concept HasSerialize = requires(C c, float& f, double& d) { // 验证存在符合约束的模板实例 c.template Serialize<float>(f); c.template Serialize<double>(d); // 若只需验证存在任意符合约束的实例,可简化为: // requires(std::floating_point T) { c.template Serialize<T>(std::declval<T&>()); } }; // 转换为传统traits结构 template <typename T> struct HasSerializeTrait : std::bool_constant<HasSerialize<T>> {}; template <typename T> inline constexpr bool HasSerialize_v = HasSerializeTrait<T>::value; int main() { static_assert(HasSerialize_v<ArbitraryClass>, "Has no Serialize"); static_assert(!HasSerialize_v<OtherClass>, "Has no Serialize"); static_assert(!HasSerialize_v<int>, "Has no Serialize"); }
方案2:基于SFINAE的传统实现
通过重载决议和SFINAE实现兼容旧标准的检测:
#include <type_traits> struct ArbitraryClass { template <std::floating_point T> void Serialize(T&) {} }; struct OtherClass {}; namespace detail { template <typename C> auto test_has_serialize(int) -> decltype( // 选用约束内的一个具体类型作为测试实例 std::declval<C>().template Serialize<float>(std::declval<float&>()), std::true_type{} ); template <typename C> std::false_type test_has_serialize(...); } template <typename T> struct HasSerialize : decltype(detail::test_has_serialize<T>(0)) {}; template <typename T> inline constexpr bool HasSerialize_v = HasSerialize<T>::value; int main() { static_assert(HasSerialize_v<ArbitraryClass>, "Has no Serialize"); static_assert(!HasSerialize_v<OtherClass>, "Has no Serialize"); static_assert(!HasSerialize_v<int>, "Has no Serialize"); }
关于“完全脱离测试类型”的说明
实际上无法完全不依赖测试类型检测模板成员函数的存在。因为模板成员函数本身不是实体,只有被实例化后才会生成具体代码。我们只能通过验证是否存在至少一个符合模板约束的实例化类型,来间接证明模板成员函数的存在。若模板有显式约束(如std::floating_point),选择该约束下的具体类型即可满足检测需求;若无约束,可选用通用类型(如int)作为测试实例。
内容的提问来源于stack exchange,提问作者Oersted

