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

Loop vector of unique_ptrs and call correct overload for run-time type

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

#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 B\n"; }
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 是一个 class 方法而不是一个自由函数,我不使用 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 方法保留在我正在访问的对象树之外。

谢谢!

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

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


1) 将type-erasure与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 << "a\n"; }    
void print_b(element& e) { std::cout << "b\n"; }

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关键字启用run-time多态性:

class element
{
private:
    virtual void f() { }

public:
    void call_f() { _f(*this); }
    virtual ~element() { }
};

void print_a(element& e) { std::cout << "a\n"; }    
void print_b(element& e) { std::cout << "b\n"; }

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".
}

为了能够在 run-time 到 select 正确的函数你需要有一些虚函数(除非你想在你的对象中自己写一些类型信息函数并添加一些调度高架)。

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

另一种可能的解决方案是使用类似 double-dispatch 的技术,使用这样的虚拟调度函数:

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();        
}

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

继承是邪恶的基础class。

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

这里是 re-expression 你的问题:

#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 B\n"; }
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,允许我select另一个实现:

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 B\n"; }
    virtual void visit(const C* c) const override { cout << "It is a C!\n"; }
};

然后给Base和所有subclasses一个调用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_casts! :)