【问题标题】:Arrays of Pointers to Abstract Types指向抽象类型的指针数组
【发布时间】:2015-12-28 21:19:22
【问题描述】:

我一直在尝试抽象类型。 下面的代码给了我想要的效果。

class base{
public:
 virtual void do_stuff() = 0;
};

class derived: public base{
public:
 void do_stuff(){/*stuff*/}
};

class manager{
 vector<shared_ptr<base>> ptrs;
public:
 void add(base* ptr){
  ptrs.emplace_back(ptr);
 }
};

manager foo;
foo.add(new derived());

很好,花花公子,但这很尴尬,因为用户不仅要处理指针,而且必须使用 new 而不调用 delete。我的问题是,是否有一种方法可以让manager 的用户不必处理指针或new

foo.add(derived()); //example

我尝试实现这一点的最终结果是:

class manager{
 vector<shared_ptr<base>> ptrs;
public:
 void add(base& ref){
  ptrs.emplace_back(&ref);
 }
};

但是,编译器说no known conversion from 'derived' to 'base&amp;'。我不知道如何使对base 的引用与对derived 的引用兼容。我该如何解决这个问题?

【问题讨论】:

  • 别忘了给base添加一个virtual析构函数。
  • 如果您的经理使用共享指针,为什么不直接将共享指针作为add 方法的参数?

标签: c++ pointers reference virtual abstract


【解决方案1】:

通过unique_ptr

您的add 函数拥有该对象的所有权。传递所有权的一种安全方式是传递unique_ptr

使用unique_ptr 相当灵活,因为您可以从unique_ptr 构造shared_ptr,或者如果您将来改变主意,您可以直接存储unique_ptr

class manager{
  vector<shared_ptr<base>> ptrs;
public:
  void add(std::unique_ptr<base> ptr){
    ptrs.emplace_back(std::move(ptr));
  }
};

manager foo;
foo.add(std::make_unique<derived>());

使用临时std::unique_ptr 可以避免拥有非异常安全的原始指针。通过使用make_unique,您可以避免写new

Live demo.

通过工厂

如果调用者真的不想处理任何类型的指针,另一个选择是传递add 函数用来构造对象的某种工厂。工厂可能只是 derived 类本身的 static create 函数:

using Factory = std::function<std::unique_ptr<base>()>;

class manager{
 std::vector<std::shared_ptr<base>> ptrs;
public:
 void addUsing(const Factory& factory){
  ptrs.emplace_back(factory());
 }
};

class derived : public base {
public:
 ...
  static std::unique_ptr<derived> create() { 
    return std::make_unique<derived>();
  }
};

manager foo;
foo.addUsing(derived::create);

Live demo.

【讨论】:

  • 非常有趣。我可能会在我的项目中使用它。没有我希望的那么优雅,但如果用户必须知道指针的存在,这并不是世界末日。
  • @WillyGoat 我添加了另一个选项,调用者不需要知道是否涉及任何指针。
【解决方案2】:

您可以让add() 函数传递要用于构造T 类型的参数,其中T 被指定为子类的类型。

template <typename T, typename... TArgs>
void add(TArgs&&... args)
{
    ptrs.emplace_back(std::make_shared<T>(std::forward<TArgs>(args)...));
}

然后可以如下调用:

bm.add<derived_a>( "hello" ); // derived_a constructor takes a string
bm.add<derived_b>( 42 );      // derived_b constructor takes an int

完整示例

#include <string>
#include <vector>
#include <memory>

class base
{
public:
    virtual void f() = 0;
};

class derived_a : public base
{
public:
    derived_a( std::string const& s ) : s_{ s } {}
    void f() override { std::cout << "derived_a::string = " << s_ << '\n'; }

private:
    std::string s_;
};

class derived_b : public base
{
public:
    derived_b( int i ) : i_{ i } {}
    void f() override { std::cout << "derived_b::int = " << i_ << '\n'; }

private:
    int i_;
};

class base_manager
{
public:
    template <typename T, typename... TArgs>
    void add( TArgs&&... args )
    {
        ptrs.emplace_back( std::make_shared<T>( std::forward<TArgs>( args )... ) );
    }

    void print() { for ( auto& d : ptrs ) d->f(); }

private:
    std::vector<std::shared_ptr<base>> ptrs;
};

int main()
{
    base_manager bm;
    bm.add<derived_a>( "hello" );
    bm.add<derived_b>( 42 );
    bm.print();
}

【讨论】:

  • 这很好,但它依赖于在编译时知道要添加的类型。通常对于多态类型,直到运行时才知道类型。
  • @ChrisDrew 我理解,但我基于他的例子,他打电话给manager.add(derived(),这暗示(从我的角度来看)他知道他要添加的类型。
  • 哇,我真的很喜欢这个结果。不幸的是,那里有很多不熟悉的代码,所以我必须先阅读一下才能使用类似的代码。
  • @WillyGoat 在这种情况下,我建议您前往:stackoverflow.com/questions/388242/…
【解决方案3】:

您不能将临时(r 值)传递给非常量引用。您还尝试获取该临时对象的地址,这最终会产生一个悬空指针和未定义的行为。

假设您想将未知运行时类型的对象传递给管理器:
您可以做的一件事是使用某种多态复制机制(如虚拟克隆方法)并在堆上制作对象的内部副本(它必须是多态的,以避免对象切片)。

class base {
public:
    virtual void do_stuff() = 0;
    virtual shared_ptr<base> clone() const = 0;
    virtual ~base()=default;
};

class derived : public base {
    int data;
public:
    derived() :data(0) {};
    derived(const derived& other) :data(other.data)
    {};
    virtual shared_ptr<base> clone() const override { 
        return make_shared<derived>(*this); 
    };
    void do_stuff() {/*stuff*/ }
};

class manager {
    vector<shared_ptr<base>> ptrs;
public:
    void add(const base& obj) {
        ptrs.emplace_back(obj.clone());
    }
};
int main() {
    manager foo;
    foo.add(derived());
}

没有clone,它看起来像这样:

void add(const base& obj) {
    if (typeid(obj)== typeid(derived) ){
        ptrs.emplace_back(make_shared<derived>(static_cast<const derived&>(obj)));              
    }
    else if (typeid(obj) == typeid(derived2)) { 
    ...         
}

【讨论】:

  • 太好了!我认为这不能用复制构造函数来完成,对吧?我猜当你尝试emplace_back(&amp;base(obj)) 时它会遇到同样的问题?
  • @Willy Goat。您可以使用复制构造函数来执行此操作,但是您需要在 add 方法中使用 if-else 语句来创建正确类型的副本。这也有一个缺点,即每次您向项目添加一个新类时,您还必须扩展该 if-elseif-.. 语句
  • @WillyGoat 作为参考,此技术与Prototype pattern 密切相关。
  • 啊。谢谢你。我认为克隆方法是我目前首选的解决方案。
  • @WillyGoat:这个模式实际上有一个非常好的扩展,它不需要克隆方法,这在这个谈话中解释:youtube.com/watch?v=bIhUE5uUFOA。它的优点是非侵入性,但管理器的内部变得相当复杂。也许我稍后会更新我的答案。
【解决方案4】:

您最初的问题似乎与用户/调用者创建指针并将其交给并且从不删除它的事实有关。我在下面的示例只是向用户明确说明他可以将其交给并忘记它。也就是说,要求用户传递一个shared_ptr...

#include <stdlib.h>
#include <vector>
#include <memory>

using namespace std;

class base{
public:
    virtual void do_stuff() = 0;
};

class derived : public base{
public:
    void do_stuff(){/*stuff*/ }
};

class manager{
    vector<shared_ptr<base>> ptrs;
public:
    void add(shared_ptr<base> ptr){
        ptrs.emplace_back(ptr);
    }
};

int main()
{
    manager foo;
    shared_ptr<derived> bp(new derived()); //require the user supply a smart pointer
    foo.add(bp);
    return 0;
}

这比其他帖子简单,可能没有前瞻思维,但它不需要派生类实现额外的基成员。在很多情况下,这可能就足够了。

【讨论】:

  • 这比传递unique_ptr 简单吗?如果您传递shared_ptr,您将有增加和减少shared_ptr 上的引用计数的开销。也不清楚add 是否拥有所有权。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-26
  • 2013-07-08
  • 2020-03-27
  • 1970-01-01
相关资源
最近更新 更多