利用SFINAE检测Visitor中operator()的参数const属性
我正在编写一个接收如下形式Visitor的函数:
struct Visitor { void operator()(int i) {...} void operator()(const std::string& s) {...} void operator()(const auto& s) {...} };
当被访问的值是非const string时,我需要检测Visitor的operator()参数的const属性:
- 若传入的
Visitor是以下两种情况,要检测出它必然接收const引用:struct Visitor { void operator()(const std::string& s) {...} void operator()(const auto& s) {...} };struct Visitor { void operator()(const auto& s) {...} }; - 若传入的
Visitor是以下两种情况,要检测出它接收的是非const引用:struct Visitor { void operator()(std::string& s) {...} void operator()(auto& s) {...} };struct Visitor { void operator()(const std::string& s) {...} void operator()(auto& s) {...} };
我尝试借助模板,利用重载歧义错误来检测结构体是否包含特定参数类型的operator()。非模板场景下的测试方式如下:
struct TestConstStringArg { void operator()(const std::string& str) {} }; struct Tester : public TestConstStringArg, public Visitor { void tester() { std::string s; (*this)(s); } };
若Visitor也包含operator()(const std::string&)成员,会因函数解析歧义产生编译错误。
我想将此逻辑转化为模板,得到可检查的constexpr bool,当前实现如下:
template <class T, class ArgTest> struct ArgTester : public T, public ArgTest { void tester() { std::string s; (*this)(s); } }; template<class T> struct VisitorArgTester { typedef char yes[1]; typedef char no[2]; template<class ArgTest> static no& test(ArgTester<T, ArgTest>* t); template<class ArgTest> static yes& test(...); static constexpr bool hasConstStr = sizeof(test<TestConstStringArg>(nullptr)) == sizeof(yes); }; struct Visitor { void operator()(const std::string& str) {} }; static_assert(VisitorArgTester<Visitor>::hasConstStr);
我的思路是:若T不包含const string引用参数的operator(),返回no&的test版本可正常编译;否则歧义错误会使返回yes&的版本被选用。但实际不管Visitor是否有对应operator(),static_assert始终失败,请问我哪里出错了?
你的实现思路存在核心错误:SFINAE(替换失败并非错误)机制不会处理函数调用时的重载歧义错误。
当你实例化ArgTester<T, TestConstStringArg>并尝试调用(*this)(s)时,如果T和TestConstStringArg都有匹配的operator(),会产生重载歧义——这属于硬编译错误,不属于模板参数替换阶段的错误,因此编译器不会触发SFINAE去选择test(...)的重载,反而会直接报错(只是你的代码里因为test的模板参数推导逻辑,编译器没有直接抛出错误,但实际上ArgTester的tester函数中的歧义会导致test<TestConstStringArg>的实例化失败,此时编译器会直接选择test(...),所以sizeof始终是no的大小)。
正确的检测方案
我们可以通过表达式SFINAE直接检测Visitor能否接受非const的std::string&参数:
- 如果
Visitor可以接受std::string&,说明它有非const的重载(或者模板重载能匹配非const引用); - 如果只能接受
const std::string&,则说明它的重载都是const引用类型。
以下是具体实现(兼容C++11及以上):
#include <string> #include <type_traits> // 辅助模板,用于检测是否能调用 Visitor::operator()(std::string&) template <class Visitor, class = void> struct accepts_nonconst_string_ref : std::false_type {}; template <class Visitor> struct accepts_nonconst_string_ref<Visitor, decltype(std::declval<Visitor>()(std::declval<std::string&>()))> : std::true_type {}; // 最终的检测逻辑 template <class Visitor> struct VisitorArgChecker { // 如果能接受非const string&,则说明是接收非const引用 static constexpr bool uses_nonconst_ref = accepts_nonconst_string_ref<Visitor>::value; // 反之则必然接收const引用 static constexpr bool uses_const_ref = !uses_nonconst_ref; }; // 测试用例1:只有const string&重载 struct ConstVisitor1 { void operator()(const std::string& s) {} void operator()(const auto& s) {} }; static_assert(VisitorArgChecker<ConstVisitor1>::uses_const_ref); static_assert(!VisitorArgChecker<ConstVisitor1>::uses_nonconst_ref); // 测试用例2:只有const auto&重载 struct ConstVisitor2 { void operator()(const auto& s) {} }; static_assert(VisitorArgChecker<ConstVisitor2>::uses_const_ref); static_assert(!VisitorArgChecker<ConstVisitor2>::uses_nonconst_ref); // 测试用例3:有非const string&和auto&重载 struct NonConstVisitor1 { void operator()(std::string& s) {} void operator()(auto& s) {} }; static_assert(VisitorArgChecker<NonConstVisitor1>::uses_nonconst_ref); static_assert(!VisitorArgChecker<NonConstVisitor1>::uses_const_ref); // 测试用例4:有const string&和auto&重载 struct NonConstVisitor2 { void operator()(const std::string& s) {} void operator()(auto& s) {} }; static_assert(VisitorArgChecker<NonConstVisitor2>::uses_nonconst_ref); static_assert(!VisitorArgChecker<NonConstVisitor2>::uses_const_ref);
原理说明
accepts_nonconst_string_ref模板利用SFINAE:如果std::declval<Visitor>()(std::declval<std::string&>())这个表达式是合法的(即Visitor可以接受非const的std::string&参数),则特化版本会被选中,值为true;否则主模板生效,值为false。VisitorArgChecker基于上述结果,直接推导Visitor使用的是const还是非const引用:只要能接受非const引用,就判定为使用非const;否则必然只能接受const引用。
这种方法直接针对你需要检测的核心行为(能否接受非const字符串引用),避免了原方案中重载歧义带来的硬错误问题,逻辑更直接可靠。
内容的提问来源于stack exchange,提问作者gimmeamilk

