【问题标题】:Call base member after virtual dispatching (emulate virtual destructor-like dispatching)虚拟调度后调用基成员(模拟类似虚拟析构函数的调度)
【发布时间】:2015-12-15 10:59:49
【问题描述】:

虚拟调度只是在层次结构中选择要调用的“正确”事物。

我想实现类似于析构函数行为的功能,其中首先调用派生析构函数,然后调用层次结构中的所有析构函数,直到最上层。

客户端应该只定义成员函数,调用顺序应该自动解析。我有一个使用 CRTP 且没有虚拟呼叫的解决方案草案;它有它的优点和缺点:

#include <iostream>
#include <memory>

using namespace std;

template<class T>
struct Dispatcher
{
    void f() {
        ((T*)this)->f();
        std::cout << "calling f base\n"; 
    }
};
    
struct Implementation : Dispatcher<Implementation>
{
    void f() {
        std::cout << "calling f derived\n"; 
    }
};
    
int main()
{
    shared_ptr<Dispatcher<Implementation>> obj = make_shared<Implementation>(); 
    obj->f(); 
}

Demo

这样用户只需从调度程序派生并定义她感兴趣的方法(这意味着调度程序定义了所有需要的接口)

缺点是引入了 CRTP 的所有缺点,我并没有真正的虚拟调度。有没有更简洁和惯用的方法来实现这一点?可以为更深入的层次结构完成吗?

编辑

我有一个重置方法,我希望调用Derived::reset 来触发对Base1::reset 的调用直到BaseN::reset,而无需用户实际编写对这些函数的调用,就像调用析构函数会触发一样在层次结构中调用析构函数。

【问题讨论】:

  • Implementation::f 中调用Dispatcher&lt;Implementation&gt;::f(); 怎么样? coliru.stacked-crooked.com/a/6fc7f81b756838ad
  • @DenisBlank 重点是让客户端免于手动进行此类调用。如果我要对“如何编写成员函数”施加额外的限制,那么虚拟调度就足够了。同样,我正在寻找一种模拟虚拟析构函数行为的方法;您现在不会在那里手动调用基本析构函数,对吗?
  • 你真的在考虑使用这个可憎的东西吗?真正的虚拟调度有什么问题?
  • @BЈовић 真正的虚拟调度在调用相关的基础方法后不会调用基础方法。有什么不清楚的地方?假设我有一个重置方法,并且我希望调用Derived::reset 来触发对Base1::reset 的调用直到BaseN::reset,而无需用户实际编写对这些函数的调用(就像调用析构函数会触发调用析构函数一样向上层级)。这个概念你清楚吗?
  • 我不认为这是可能的,基类不应该“知道”它的派生类比虚拟表更远,那么如果派生类想要调用层次结构树中的函数就起来了给他们。我知道的唯一解决方法是 CRTP,我看不出有什么方法可以超越一个层次结构级别。但是您可以创建一个单独的包装器虚函数来调用当前的重置,然后将自身调用上一层,您可以将定义放在某个宏中,这样此类的客户端只需添加宏即可。不完全是您正在寻找的东西,但可以解决问题。

标签: c++


【解决方案1】:

感谢std::tr2::direct_bases,这实际上非常简单。希望这能在未来成为一些 C++ 标准,但现在下面的例子实际上是在 gcc 上编译的。

首先,让我们为类型特征命名。以下为我们提供了一个std::tr2::__reflection_typelist,其中包含T 的所有直接基础:

template <typename T>
using direct_bases_t = typename std::tr2::direct_bases<T>::type;

所以对于这个层次结构:

struct A { };
struct B : A { };
struct C : B { };

对于A,它将给出一个空列表,对于C,它将给出一个仅包含B 的列表。 (如果您不想使用direct_bases,则必须明确提供基类,请参阅答案末尾)。我们所要做的就是迭代:

template <typename T, typename F, typename... Bases>
void for_each_base(T* t, F f, std::tr2::__reflection_typelist<Bases...> )
{
    using expander = int[];
    expander{0,
        (void(
            for_each_base(static_cast<Bases*>(t), f)
        ), 0)...
    };
}

template <typename T, typename F>
void for_each_base(T* t, F f)
{
    f(t);
    for_each_base(t, f, direct_bases_t<T>{});
}

这将在对象上调用f,然后向上遍历层次结构直到完成。所以用一个特别简单的函子:

struct Logger {
    template <typename T>
    void operator()(T* ) {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }
};

int main() {
    C c;
    for_each_base(&c, Logger{});
}

输出是:

void Logger::operator()(T*) [with T = C]
void Logger::operator()(T*) [with T = B]
void Logger::operator()(T*) [with T = A]

Demo

对于您的情况,您基本上只需要:

for_each_base(obj, [](auto p){ p->reset(); });

如果没有direct_bases,我们只需要为每个类提供我们自己的类型列表和我们自己的手动基础列表。比如:

template <typename... > struct typelist { };

struct A { using bases = typelist<>; };
struct B : A { using bases = typelist<A>; };
struct C : B { using bases = typelist<B>; };

template <typename T>
using direct_bases_t = typename T::bases;

否则解决方案的结构是相同的(尽管现在使用typelist 而不是std::tr2::__reflection_typelist)。

【讨论】:

    【解决方案2】:

    您想为某些函数模仿类似析构函数的行为。

    模仿析构函数行为的最聪明的方法是使用析构函数。

    我的意思是在你的类的一些并行层次结构的析构函数中调用这些函数。

    首先你需要知道基类——我尝试使用 __direct_bases——gcc 的新扩展——但是如果失败了 (gcc bug),那么让我们使用 C++ 解决方案:

    template <typename T>
    using GetBaseType = typename T::Base;
    using NoBase = void;
    

    以上要求此层次结构中的每个类都必须具有Base typedef - 这可以在未来的 C++ 版本中解决。

    所以有了基类 - 我们可以创建两个层次结构 - 一个用于非常量,一个用于 const 函数:

    template <typename T>
    class CallUp;
    
    template <>
    class CallUp<NoBase>
    {
    public:
       template <typename Op, typename T>
       CallUp(Op&& ignoreOperation, T&& ignoreObject) {}
    };
    
    
    template <typename T>
    class CallUp : public CallUp<GetBaseType<T>>
    {
    public:
        template <typename Op>
        CallUp(Op&& op, T& obj) : CallUp<GetBaseType<T>>(op, obj), op(std::forward<Op>(op)), obj(obj)
        {}
        ~CallUp()
        {
            op(obj);
        }
    private:
        std::function<void(T&)> op;
        T& obj;
    };
    
    template <typename T>
    class ConstCallUp;
    
    template <>
    class ConstCallUp<NoBase>
    {
    public:
       template <typename Op, typename T>
       ConstCallUp(Op&& ignoreOperation, T&& ignoreObject) {}
    };
    
    template <typename T>
    class ConstCallUp : public ConstCallUp<GetBaseType<T>>
    {
    public:
        template <typename Op>
        ConstCallUp(Op&& op, const T& obj) : ConstCallUp<GetBaseType<T>>(op, obj), op(std::forward<Op>(op)), obj(obj)
        {}
        ~ConstCallUp()
        {
            op(obj);
        }
    private:
        std::function<void(T&)> op;
        const T& obj;
    };
    
    template <typename Op, typename T>
    auto callUp(Op&& op, T& obj)
    {
        return CallUp<T>(std::forward<Op>(op), obj);
    }
    template <typename Op, typename T>
    auto callUp(Op&& op, const T& obj)
    {
        return ConstCallUp<T>(std::forward<Op>(op), obj);
    }
    

    callUp 层次结构的析构函数中 - 你会得到你想要的行为。

    如何调用这些析构函数 - 只需定义该类型的临时函数 - 它将立即被销毁。

    查看使用示例:

    class Foo0
    {
    public:
        using Base = NoBase;
        void reset()
        {
            std::cout << "reset Foo0\n";
        }
        void print() const
        {
            std::cout << "Foo0\n";
        }
    };
    
    class Foo1 : public Foo0
    {
    public:
        using Base = Foo0;
        void reset()
        {
            std::cout << "reset Foo1\n";
        }
        void print() const
        {
            std::cout << "Foo1\n";
        }
    };
    
    class Foo2 : public Foo1
    {
    public:
        using Base = Foo1;
        void reset()
        {
            std::cout << "reset Foo2\n";
        }
        void print() const
        {
            std::cout << "Foo2\n";
        }
    };
    
    
    int main() {
        Foo2 foo2;
        callUp([](auto&& obj){obj.reset();}, foo2);
        callUp([](auto&& obj){obj.print();}, foo2);
    }
    

    输出:

    reset Foo2
    reset Foo1
    reset Foo0
    Foo2
    Foo1
    Foo0
    

    ideone link.


    这个框架可以扩展。 例如。多基类可以这样完成:

    template <typename ...T>
    struct MultiBases {};
    
    template <typename ...T>
    class CallUp<MultiBases<T...>> : public CallUp<T>...
    {
    public:
        template <typename Derived, typename Op>
        CallUp(Op&& op, Derived& obj) : CallUp<T>(op, obj)...
        {}
    };
    template <typename ...T>
    class ConstCallUp<MultiBases<T...>> : public ConstCallUp<T>...
    {
    public:
        template <typename Derived, typename Op>
        ConstCallUp(Op&& op, Derived& obj) : ConstCallUp<T>(op, obj)...
        {}
    };
    

    及用法示例:

    class Foo01
    {
    public:
        using Base = NoBase;
        void reset();
        void print() const;
    };
    
    class Foo02
    {
    public:
        using Base = NoBase;
        void reset();
        void print() const;
    };
    
    
    class Foo1 : public Foo01, public Foo02
    {
    public:
        using Base = MultiBases<Foo01, Foo02>;
        void reset();
        void print() const;
    };
    
    class Foo2 : public Foo1
    {
    public:
        using Base = Foo1;
        void reset();
        void print() const;
    };
    
    
    int main() {
        Foo2 foo2;
        callUp([](auto&& obj){obj.reset();}, foo2);
        callUp([](auto&& obj){obj.print();}, foo2);
    }
    

    输出:

    reset Foo2
    reset Foo1
    reset Foo02
    reset Foo01
    Foo2
    Foo1
    Foo02
    Foo01
    

    Ideone link


    以非虚方式调用虚函数的问题可以在lambda级别解决。

    通过这个object_class_t 类模板,我们可以获得对象的“原始”类类型:

    template <typename T>
    using object_class_t = std::remove_cv_t<std::remove_reference_t<T>>;
    

    所以,假设我们有任何虚拟重置函数 - 并且希望以非虚拟方式调用它们(期望的行为):

    int main() {
        Foo2 foo2;
        auto nonVirtualReset = [](auto&& obj)
        {
             using ObjClass = object_class_t<decltype(obj)>;
             obj.ObjClass::reset();
        };
        callUp(nonVirtualReset, foo2);
        callUp([](auto&& obj){obj.print();}, foo2);
    }
    

    【讨论】:

    • @stgatilov - 我简化了我的解决方案。 .
    • 是的,现在确实好多了!用户只需在每个类中创建Base typedef(如果std::bases 之类的东西成为标准,也许可以避免)。做一个方法调用不是很漂亮,但是如果需要的话很容易包装到宏中。此外,可以在 lambda 中做许多不同的事情(不仅仅是一个方法调用)。顺便说一句:它是否适用于多重继承?虚拟继承?
    • 另外,这种方式似乎不能直接调用虚函数(至少我不知道如何在lambda中进行虚方法的非虚调用)。可以通过将方法实现转移到非虚拟方法中来规避它。
    • @stgatilov 它需要更多的工作-但可以以非虚拟方式调用虚拟函数-而不是obj.foo()-必须调用obj.ClassName::foo()-但要创建这样的设计是不像用简单的 lambda 调用那么容易。获得许多基类——这也是可行的——但会使事情复杂化。我会尝试回到这个答案(今天?),我会提供更新。获得虚拟基地的行为——这将是一个艰难的目标——但也是可行的。请让我知道哪些是您最“必备”的?我可以从那个开始。
    • ConstCallUp&lt;T&gt; 完全没有必要,你可以使用CallUp&lt;const T&gt;。此外,鉴于您要求您的类定义自己的基础,您不需要在析构函数中编写所有逻辑(这不必要地复杂) - 您可以直接编写它。
    【解决方案3】:

    这是一种不会侵入类主体的方法 在您的层次结构中,但要求您根据 一个特殊规则,其中基类始终是以下各项的实例:

    template<class Parent, class Child = void> struct chain;
    

    初步草图:如果您通常将层次结构表示为:

    struct base 
    { ... virtual void reset(); ... };
    struct child : base 
    { ... virtual void reset(); ... };
    struct grandchild : child 
    { ... virtual void reset(); ... };
    struct great_grandchild : grandchild 
    { ... virtual void reset(); ... };
    struct great_great_grandchild : great_grandchild 
    { ... virtual void reset(); ... };
    

    然后你会这样表达:

    struct base 
    { ... virtual void reset(); ... };
    struct child : chain<base> 
    { ... virtual void reset(); ... };
    struct grandchild : chain<base,child> 
    { ... virtual void reset(); ... };
    struct great_grandchild : chain<child,grandchild> 
    { ... virtual void reset(); ... };
    struct great_great_grandchild : chain<grandchild,great_grandchild> 
    { ... virtual void reset(); ... };
    

    正如草图所揭示的,该解决方案提供了真正的多态层次结构, 可以是任何深度。

    类的普通成员函数可以以普通方式调用,但是 “链式”成员函数 - 具有类似析构函数的递归 - 必须是 在 chain&lt;P&gt;chain&lt;P,C&gt; 类型的某些对象上调用,其中 CP 的分层子级。

    chain&lt;grandchild,child&gt;great_grandchild 等基类, 用作封装向上递归调用的层次结构中的链接 的reset。它将grandchildchild 作为虚拟基类 - 使它们也成为great_grandchild 的虚拟基类。 因此代表great_grandchild 它可以同时调用this-&gt;grandchild::reset()this-&gt;child::reset(),然后代表child递归请求like。 递归在特化 chain&lt;T&gt; 中终止:因此:

    struct child : chain<base> { ... };
    

    这里是chain的定义

    #include <type_traits>
    
    namespace detail {
    
        template<class ...> using void_t = void;
    
        template<class, class = void >
        struct has_base_type : std::false_type {};
    
        template<class T >
        struct has_base_type<T,void_t<typename T::base_type>> : std::true_type
        {
            using type = typename T::base_type;
        };
    
        template<class T>
        using has_base_type_t = typename has_base_type<T>::type;
    
        template<class T>
        struct has_real_base
        {
            static constexpr bool value = has_base_type<T>::value && 
                !std::is_same<has_base_type_t<T>,T>::value;
        };
    
        template<class Parent, class Child = void>
        struct chain : virtual Parent, virtual Child
        {
            using base_type = Parent;
            using child_type = Child;
    
            template<class T>
            static std::enable_if_t<has_real_base<T>::value>
            recurse(chain * const pd) {
                pd->base_type::base_type::reset();
            }
    
            template<class T>
            static std::enable_if_t<!has_real_base<T>::value> 
            recurse(chain * const pd) {}
    
            void reset() override {
                this->child_type::reset();
                this->base_type::reset();
                recurse<base_type>(this);
            }
            virtual ~chain() = default;
        };
    
        template<class T>
        struct chain<T> : virtual T
        {
            using base_type = T;
            void reset() override {
                this->base_type::reset();
            }
            virtual ~chain() = default;
        };
    
    } // namespace detail
    
    template<class Parent, class Child = void>
    struct chain : detail::chain<Parent,Child>
    {
        using base_type = detail::chain<Parent,Child>;
        void reset() override {
            this->base_type::reset();
        }
        virtual ~chain() = default;
    };
    

    您可以附加以下内容来制作说明性程序:

    #include <iostream>
    #include <memory>
    
    struct base
    {
        virtual void reset() {
            std::cout << "calling base::reset()\n"; 
        }
        virtual ~base() = default;
    };
    
    struct child : virtual chain<base>
    {
        void reset() override {
            std::cout << "calling child::reset()\n"; 
        }
        virtual ~child() = default;
    };
    
    struct grandchild : chain<base,child>
    {
        void reset() override {
            std::cout << "calling grandchild::reset()\n";
        }
        virtual ~grandchild() = default;
    };
    
    struct great_grandchild : chain<child,grandchild>
    {
        void reset() override {
            std::cout << "calling great_grandchild::reset()\n";
        }
        virtual ~great_grandchild() = default;
    };
    
    struct great_great_grandchild : chain<grandchild,great_grandchild>
    {
        void reset() override {
            std::cout << "calling great_great_grandchild::reset()\n";
        }
        virtual ~great_great_grandchild() = default;
    };
    
    using namespace std;
    
    int main()
    {
        cout << "obj1\n";
        shared_ptr<base> obj1 = make_shared<chain<base,child>>(); 
        obj1->reset();
        cout << "\n";
        cout << "obj2\n";
        shared_ptr<base> obj2 = make_shared<chain<child,grandchild>>();
        obj2->reset();
        cout << "\n";
        cout << "obj3\n";
        shared_ptr<base> obj3 = make_shared<chain<grandchild,great_grandchild>>();
        obj3->reset();
        cout << "\n";
        cout << "obj4\n";
        shared_ptr<base> obj4 = 
            make_shared<chain<great_grandchild,great_great_grandchild>>();
        obj4->reset();
        cout << "\n";
        cout << "obj5\n";    
        shared_ptr<grandchild> obj5 = make_shared<chain<grandchild,great_grandchild>>();
        obj5->reset();
        cout << "\n";
        cout << "obj6\n";    
        shared_ptr<base> obj6 = make_shared<chain<grandchild>>();
        obj6->reset();   
    }
    

    应该输出:

    obj1
    calling child::reset()
    calling base::reset()
    
    obj2
    calling grandchild::reset()
    calling child::reset()
    calling base::reset()
    
    obj3
    calling great_grandchild::reset()
    calling grandchild::reset()
    calling child::reset()
    calling base::reset()
    
    obj4
    calling great_great_grandchild::reset()
    calling great_grandchild::reset()
    calling grandchild::reset()
    calling child::reset()
    calling base::reset()
    
    obj5
    calling great_grandchild::reset()
    calling grandchild::reset()
    calling child::reset()
    calling base::reset()
    
    obj6
    calling grandchild::reset()
    

    注意在obj5 的情况下,哪个层次类型无关紧要 - 不一定base - 链接的reset 被调用:如果它完全递归 它将一直递归到base::reset()

    注意在 obj6 的情况下,在对象上调用 reset 动态类型chain&lt;P&gt;,而不是chain&lt;P,C&gt;,抑制递归:它是 相当于调用P::reset()

    在每个函数中支持多个链式函数的进一步问题 层次类崩溃到支持一个的问题,因为 可以调用所需的任何其他成员函数。同样地 将参数传递给链式函数的问题折叠成 将一组参数传递给一个链式函数的问题。

    (g++ 5.1/clang++ 3.6, C++14, 微适应C++11)

    【讨论】:

    • 这个解决方案确实完全自动化了递归调用,但似乎对于每个层次可调用的方法,您必须在chain 类中添加它的三个实现。而且,如果你想有很多这样的类的层次结构,你必须做更多的工作......
    猜你喜欢
    • 2014-10-02
    • 2014-01-04
    • 2021-04-15
    • 2010-09-12
    • 1970-01-01
    • 2020-07-02
    • 1970-01-01
    • 2011-09-30
    • 2021-10-31
    相关资源
    最近更新 更多