【问题标题】:How to call conditionally B::f only if derived from B in C++11?仅当从 C++11 中的 B 派生时,如何有条件地调用 B::f?
【发布时间】:2016-06-13 12:34:16
【问题描述】:

如果使用静态多态性,尤其是在模板中(例如使用策略/策略模式),可能需要调用基函数成员,但您不知道实例化的类实际上是否从该基派生。

这可以很容易地用旧的 C++ 省略号重载技巧来解决:

#include <iostream>

template <class I>
struct if_derived_from
{
    template <void (I::*f)()>
    static void call(I& x) {  (x.*f)(); }

    static void call(...) { }
};

struct A    { void reset() { std::cout << "reset A" << std::endl; } };
struct B    { void reset() { std::cout << "reset B" << std::endl; } };
struct C    { void reset() { std::cout << "reset C" << std::endl; } };
struct E: C { void reset() { std::cout << "reset E" << std::endl; } };
struct D: E {};

struct X: A, D {};

int main()
{
    X x;
    if_derived_from<A>::call<&A::reset>(x);
    if_derived_from<B>::call<&B::reset>(x);
    if_derived_from<C>::call<&C::reset>(x);
    if_derived_from<E>::call<&E::reset>(x);
    return 0;
}

问题是:

  • 有没有更好的简单方法(例如 SFINAE 看起来不是这样)在 C++11/C++14 中实现相同的结果?
  • 优化编译器会省略省略号参数函数的空调用吗?希望这种情况对任何“正常”功能都不是特别的。

【问题讨论】:

    标签: c++ templates c++11 c++14


    【解决方案1】:

    一种选择是引入两个不同优先级的重载,并为首选的一个配备表达式 SFINAE。

    #include <utility>
    
    template <typename T, typename... Args, typename C, typename R, typename... Params>
    auto call_impl(int, R(C::*f)(Args...), T&& t, Params&&... params)
        -> decltype((std::forward<T>(t).*f)(std::forward<Params>(params)...))
    {
        return (std::forward<T>(t).*f)(std::forward<Params>(params)...);
    }
    
    template <typename T, typename... Args, typename C, typename R, typename... Params>
    void call_impl(char, R(C::*)(Args...), T&&, Params&&...)
    {
    }
    
    template <typename T, typename... Args, typename C, typename R, typename... Params>
    auto call(R(C::*f)(Args...), T&& t, Params&&... params)
        -> decltype(call_impl(0, f, std::forward<T>(t), std::forward<Params>(params)...))
    {
        return call_impl(0, f, std::forward<T>(t), std::forward<Params>(params)...);
    }
    

    测试:

    int main()
    {
        X x;
        call(&B::reset, x);
    }
    

    DEMO

    将首先通过重载决议选择上层函数(由于 0int 的精确匹配),如果 (t.*f)(params...) 无效,则可能从可行候选集中排除。在后一种情况下,对call_impl 的调用会退回到第二个重载,即无操作。


    鉴于&amp;A::reset 可能由于多种原因而失败,并且您可能不一定要显式指定函数的签名,并且最重要的是,如果成员函数存在,您希望调用失败,但它不会匹配函数调用参数,然后您可以利用通用 lambda:

    #include <utility>
    #include <type_traits>
    
    template <typename B, typename T, typename F
            , std::enable_if_t<std::is_base_of<B, std::decay_t<T>>{}, int> = 0>
    auto call(T&& t, F&& f)
        -> decltype(std::forward<F>(f)(std::forward<T>(t)))
    {
        return std::forward<F>(f)(std::forward<T>(t));
    }
    
    template <typename B, typename T, typename F
            , std::enable_if_t<!std::is_base_of<B, std::decay_t<T>>{}, int> = 0>
    void call(T&& t, F&& f)
    {
    }
    

    测试:

    int main()
    {
        X x;
        call<A>(x, [&](auto&& p) { return p.A::reset(); });
        call<B>(x, [&](auto&& p) { return p.B::reset(); });
    }
    

    DEMO 2

    【讨论】:

    • 0 精确/隐式匹配的好技巧,谢谢!此解决方案的唯一问题是,当 X 派生自 A 并且 A 具有 reset() 函数成员但具有不同的签名时,它不会编译失败,例如答::重置(整数)。在这种情况下,它会默默地忽略可能不受欢迎的 A::reset() 调用。
    • @Rost 那么你会像call(&amp;A::reset, x, 42); 一样称呼它,但是你想要call(&amp;A::reset, x); 的编译错误?
    • Piotr,是的,这是可取的。
    • Piotr,是的,这可以解决问题,但不幸的是,简单性逃脱了......无论如何,谢谢!
    • @Rost 那么可能是this ?对于任何过载(请注意,&amp;A::reset 如果过载将不起作用)和任何 args 不匹配,这将始终有效
    【解决方案2】:

    类似的东西呢:

    #include <iostream>
    #include <type_traits>
    
    struct A    { void reset() { std::cout << "reset A" << std::endl; } };
    struct B    { void reset() { std::cout << "reset B" << std::endl; } };
    
    struct X :public A{};
    
    template <typename T, typename R, typename BT>
    typename std::enable_if<std::is_base_of<BT, T>::value, R>::type
    call_if_possible(T & obj, R(BT::*mf)())
    {
        return (obj.*mf)();
    }
    
    template <typename T, typename R, typename BT>
    typename std::enable_if<!std::is_base_of<BT, T>::value, R>::type
    call_if_possible(T & obj, R(BT::*mf)()) { }
    
    int main()
    {
        X x;
    
        call_if_possible(x, &A::reset);
        call_if_possible(x, &B::reset);
    }
    

    ideone

    编辑

    也许更易读的方式:

    template <typename T, typename R, typename BT>
    R call_if_possible_impl(T & obj, R(BT::*mf)(), std::false_type){}
    
    template <typename T, typename R, typename BT>
    R call_if_possible_impl(T & obj, R(BT::*mf)(), std::true_type)
    {
        return (obj.*mf)();
    }
    
    template <typename T, typename R, typename BT>
    R call_if_possible(T & obj, R(BT::*mf)())
    {
        return call_if_possible_impl(obj, mf, typename std::is_base_of<BT, T>::type());
    }
    

    ideone

    【讨论】:

    • 使用 std::void_t 可能更具可读性(我会尝试写那个)...我不确定如何在第二个专业化中摆脱多余的 enable_if
    • 但我发现它比省略号版本更具可读性(我不太喜欢省略号)
    • @IsmaelMiguel 那是因为它不是 C 或 C# ;)
    • @IsmaelMiguel 它“是”stackoverflow.com/questions/4326045/…
    • 我使用成员函数而不是 lambda 来尽可能准确地回答您的问题;)
    【解决方案3】:

    基于@PiotrSkotnicki 和@relaxxx 之前提供的答案,我想结合最简单和可读的解决方案,而不是 SFINAE 和其他眼睛里的东西。仅供参考,不予采纳:

    #include <iostream>
    #include <type_traits>
    
    template <class Base, class Derived>
    using check_base = typename std::is_base_of<Base, Derived>::type;
    
    template <class Base, class Derived, typename Func>
    void call(Derived& d, Func&& f)
    {
        call<Base>(d, std::forward<Func>(f), check_base<Base, Derived>());
    }
    
    template <class Base, typename Func>
    void call(Base& b, Func&& f, std::true_type)
    {
        f(b);
    }
    
    template <class Base, class Derived, typename Func>
    void call(Derived&, Func&&, std::false_type)
    {
    }
    
    struct A    { void reset(int i) { std::cout << "reset A: " << i << std::endl;} };
    struct B    { void reset()      { std::cout << "reset B" << std::endl;} };
    struct C    { void reset()      { std::cout << "reset C" << std::endl;} };
    struct E: C { void reset()      { std::cout << "reset E" << std::endl;} };
    
    struct D: A, E {};
    
    int main()
    {
        D d;
        int i = 42;
        call<A>(d, [&](auto& p) { p.reset(i); } );
        call<B>(d, [](auto& p)  { p.reset(); } );
        call<C>(d, [](auto& p)  { p.reset(); } );
        call<E>(d, [](auto& p)  { p.reset(); } );
    }
    

    直播地址:http://cpp.sh/5tqa

    【讨论】:

    • 注意static_cast&lt;Base&amp;&gt;(d).reset()d.Base::reset()是有区别的,前者涉及后期绑定,后者不涉及
    • @PiotrSkotnicki,是的,我理解,但它不打算与虚函数一起使用。这只是关于静态多态性。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多