循环unique_ptrs的向量,并为运行时类型调用正确的重载



对于共享公共基类的对象,我有一个vectorunique_ptrs。我想遍历向量,并根据存储的类型调用函数的正确重载。问题是,这个函数不是类的成员(对于那些喜欢谈论设计模式的人来说:假设我正在实现一个访问者类,fVisit方法)。考虑以下代码示例(或在线尝试):

#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class Base{ public: virtual ~Base() {}; };
class A : public Base { public: virtual ~A() {} };
class B : public Base { public: virtual ~B() {} };
class C : public Base { public: virtual ~C() {} };
void f(Base* b) { cout << "Calling Base :(n"; }
void f(A* a) { cout << "It is an A!n"; }
void f(B* b) { cout << "It is a Bn"; }
void f(C* c) { cout << "It is a C!n"; }
template<class Derived>
void push(vector<unique_ptr<Base>>& v, Derived* obj)
{
    v.push_back(std::unique_ptr<Derived>{obj});
}
int main() {
    vector<unique_ptr<Base>> v{};
    push(v, new A{});
    push(v, new B{});
    push(v, new C{});
    for(auto& obj : v)
    {
        f(obj.get());
    }
    return 0;
}

我的代码有一些表面上的差异(f是一个类方法,而不是一个自由函数,我不使用using namespace std),但这表明了总体思想。我看到

Calling Base :(
Calling Base :(
Calling Base :(

而我喜欢

It is an A!
It is a B!
It is a C!

我想知道我是否可以获得要调用的f的正确过载(我想完全去掉f(Base*)版本)。

一种选择是沿着的路线进行手动类型检查

     if(dynamic_cast<A>(obj) != nullptr) f((A*)obj);
else if(dynamic_cast<B>(obj) != nullptr) f((B*)obj);
...

但这实在太难看了。另一种选择是将f移动到Base,但如上所述,我正在实现访问者模式,并且更愿意将Visit方法排除在我正在访问的对象树之外。

谢谢!

EDIT:显然,我的代码示例给人的印象是,我的类型必须是非虚拟的——实际上,我对添加虚拟方法没有根本的反对意见,所以我将其添加到了代码示例中。

为了能够在运行时选择正确的函数,您需要一些虚拟函数(除非您想在对象中为自己编写一些类型信息函数并添加一些调度开销)。

最简单的方法是使f()成为基类的虚拟成员函数,并为每个派生类型提供覆盖版本。但你已经取消了这种方法。

另一种可能的解决方案是使用类似双重调度的技术,使用这样的虚拟调度功能:

class Base { 
public:  
   virtual void callf() { f(this); } 
   virtual ~Base() {}
}
class A : public Base {
public: 
   void callf() override { f(this) };  // repeat in all derivates !  
}
class B : public Base {
public: 
   void callf() override { f(this) };  // repeat in all derivates !   
}
...
void F(Base *o) {  // this is the function to be called in your loop 
    o->f();        
}

诀窍是编译器将使用this的真实类型在每个callf()函数中找到正确的f()函数
然后,F()函数将调用虚拟调度函数,确保它在执行时与对象的真实类型相对应。

这里有两种可能的解决方案。


1) 与std::function一起使用类型擦除并避免继承:

class element
{
private:
    std::function<void(element&)> _f;
public:
    template<typename TF>
    element(TF&& f) : _f(std::forward<TF>(f)) { }
    void call_f() { _f(*this); }
};
void print_a(element& e) { std::cout << "an"; }    
void print_b(element& e) { std::cout << "bn"; }
auto make_element_a() { return element{&print_a}; }    
auto make_element_b() { return element{&print_b}; }
int main()
{
    std::vector<element> es;
    es.emplace_back(make_element_a());
    es.emplace_back(make_element_a());
    es.emplace_back(make_element_b());
    es.emplace_back(make_element_a());
    for(auto& e : es) e.call_f();
    // Will print: "a a b a".
}

2) 使用virtual关键字启用运行时多态性:

class element
{
private:
    virtual void f() { }
public:
    void call_f() { _f(*this); }
    virtual ~element() { }
};
void print_a(element& e) { std::cout << "an"; }    
void print_b(element& e) { std::cout << "bn"; }
class a : public element
{
    void f() override { print_a(*this); }
};
class b : public element
{
    void f() override { print_b(*this); }
};
int main()
{
    std::vector<std::unique_ptr<element>> es;
    es.emplace_back(std::make_unique<a>());
    es.emplace_back(std::make_unique<a>());
    es.emplace_back(std::make_unique<b>());
    es.emplace_back(std::make_unique<a>());
    for(auto& e : es) e.call_f();
    // Will print: "a a b a".
}

继承是邪恶的基类。

通过在句柄类的内部实现中隐藏这些对象之间的关系,我们可以创建一个所有对象都通用的接口,即使这些对象不是从公共基派生的。

以下是你的问题的再次表述:

#include <iostream>
#include <memory>
#include <vector>
#include <type_traits>
#include <utility>
class A {};  // note! no inheritance at all
class B {};
class C {};
void f(A& a) { std::cout << "It is an A!n"; }
void f(B& b) { std::cout << "It is a Bn"; }
void f(C& c) { std::cout << "It is a C!n"; }
struct fcaller
{
    struct concept
    {
        virtual void call_f() = 0;
        virtual ~concept() = default;
    };
    template<class T>
    struct model final : concept
    {
        model(T&& t) : _t(std::move(t)) {}
        void call_f() override {
            f(_t);
        }
        T _t;
    };
    template<class T, std::enable_if_t<not std::is_base_of<fcaller, std::decay_t<T>>::value>* = nullptr >
    fcaller(T&& t) : _impl(std::make_unique<model<T>>(std::forward<T>(t))) {}
    void call_f()
    {
        _impl->call_f();
    }
private:

    std::unique_ptr<concept> _impl;
};
int main() {
    using namespace std;
    vector<fcaller> v{};
    v.emplace_back(A{});
    v.emplace_back(B{});
    v.emplace_back(C{});
    for(auto& obj : v)
    {
        obj.call_f();
    }
    return 0;
}

输出:

It is an A!
It is a B
It is a C!

感谢大家的回复。问题似乎是,我意识到太晚了(在编辑我的OP并对您的澄清请求写评论时),我正在实现现有的设计模式。结果我把模式倒过来了。

我应该做的是在f之上创建一个接口类,允许我选择另一个实现:

class IVisitor 
{ public:
    virtual void visit(const A*) const = 0; 
    virtual void visit(const B*) const = 0; 
    virtual void visit(const C*) const = 0; 
};
// Example implementation: 
class TypePrinter: public IVisitor
{ public:
    // Base* version not needed - yay!
    // virtual void visit(const Base*) const { std::cout << "Called Base :(n"; }
    virtual void visit(const A* a) const override { cout << "It is an A!n"; }
    virtual void visit(const B* b) const override { cout << "It is a Bn"; }
    virtual void visit(const C* c) const override { cout << "It is a C!n"; }
};

然后给Base和所有子类一个调用visit:的方法

struct A : public Base 
{ public: 
    // This line copied across to `B` and `C` as well:
    virtual void accept(const IVisitor& v) override { v.visit(this); } 
};

现在,正如@LogicStuff正确指出的那样,多态性将很好地解决过载问题:

TypePrinter visitor;
for(auto& obj : v)
{
    obj->accept(visitor);
}

看妈妈,没有dynamic_cast s!:)

相关内容

  • 没有找到相关文章

最新更新