如何在C++继承场景下通过SFINAE检测基类的protected成员方法
这个问题我之前也碰到过,核心原因确实是你猜的那样:std::declval<Base>()生成的是一个独立的Base类型对象,哪怕你在Derived类里面,直接访问这个对象的protected成员也是不允许的——C++的访问规则里,派生类只能访问自身基类子对象或者同类型派生类对象的基类子对象的protected成员,独立的Base对象的protected成员哪怕在派生类里也碰不到。
那怎么改呢?其实很简单,我们换个思路:既然Derived继承了Base,那Derived的对象肯定有权限调用Base的protected foo,那我们就检测Derived类是否能调用foo,而不是直接检测Base的对象。因为如果Base有protected的foo,Derived继承后自然拥有这个方法的访问权,所以检测Derived的对象是否能调用foo,就等价于检测Base是否有这个方法。
修改后的代码可以这样写:
template <class Base> class Derived : public Base { // 现在检测的是Derived<Type>是否能调用foo,而不是直接用Base template <class Type> static auto test_foo(int) -> decltype( std::declval<Derived<Type>>().foo(std::declval<Bar&>()), std::true_type() ); template <class> static auto test_foo(...) -> std::false_type; // 这里传入Base作为Type,检测Derived<Base>是否有foo using has_foo = decltype(test_foo<Base>(0)); public: constexpr void process_bar(Bar& bar) { if constexpr (has_foo{}) Base::foo(bar); // ... 其他业务逻辑 } };
为什么这样能行?因为std::declval<Derived<Base>>()生成的是Derived类型的右值,Derived作为Base的派生类,完全有权限调用继承来的protected foo方法,所以SFINAE会正确识别到Base存在foo的情况,返回std::true_type。
如果你担心Derived自己可能会定义foo(虽然你的代码里没有),也可以更明确地指定调用Base的foo,比如在检测表达式里写:
std::declval<Derived<Type>>().Base::foo(std::declval<Bar&>())
这样就严格检测Base类是否存在foo方法,而不会被Derived自己的foo干扰。
还有一种更灵活的方式,如果你不想依赖Derived的类型,可以写一个临时的辅助派生类在test_foo里面:
template <class Base> class Derived : public Base { template <class Type> static auto test_foo(int) -> decltype( // 临时定义一个从Type继承的辅助类,用它来访问protected成员 []() -> struct Helper : Type { using Type::foo; }& { static Helper h; return h; }().foo(std::declval<Bar&>()), std::true_type() ); template <class> static auto test_foo(...) -> std::false_type; using has_foo = decltype(test_foo<Base>(0)); public: constexpr void process_bar(Bar& bar) { if constexpr (has_foo{}) Base::foo(bar); // ... } };
不过这种方法比第一种稍微复杂一点,第一种直接用当前的Derived类更直观,也符合你的代码结构。
最后要注意的是,C++17及以后的if constexpr才能正确处理这个编译期分支,你的代码里已经用了,所以没问题。
内容来源于stack exchange

