STD容器中的抽象类



在编程时,我经常使用多态性,因为它自然地为我需要的对象建模。另一方面,我经常使用标准容器来存储这些对象,并且我倾向于避免使用指针,因为这要么要求我释放对象,而不是将它们从堆栈中弹出,要么要求我在使用指针时确定对象将留在堆栈中。当然,有各种各样的指针容器对象可以帮你完成这个任务,但在我的经验中,它们也不理想,甚至令人讨厌。这是;如果存在这样一个简单的解决方案,它应该是在c++语言中,对吗?;)

我们来举一个经典的例子:

#include <iostream>
#include <vector>
struct foo {};
struct goo : public foo {};
struct moo : public foo {};
int main() {
    std::vector<foo> foos;
    foos.push_back(moo());
    foos.push_back(goo());
    foos.push_back(goo());
    foos.push_back(moo());
    return 0;
}

参见:http://ideone.com/aEVoSi。这工作得很好,如果对象有不同的sizeof,编译器可能会应用切片。然而,由于c++不像Java那样知道instanceof,而且据我所知,也没有合适的替代方法存在,因此在从vector中作为foo获取继承类的属性后,无法访问它们的属性。

因此可以使用虚函数,但这不允许分配foo,因此不允许在vector中使用它们。请参阅为什么不能声明std::vector?.

例如,我可能希望能够打印两个子类,简单的功能,对吗?

#include <iostream>
#include <vector>
struct foo {
        virtual void print() =0;
        virtual ~foo() {}
};
struct goo : public foo {
    int a;
    void print() { std::cout << "goo"; }
};
struct moo : public foo {
    int a,b;
    void print() { std::cout << "moo"; }
};
int main() {
    std::vector<foo> foos;
    foos.push_back(moo());
    foos.push_back(goo());
    foos.push_back(goo());
    foos.push_back(moo());
    for(foo& f : foos) {
        f.print();
    }
    return 0;
}

来源:http://ideone.com/I4rYn9

这是一个简单的添加,作为一个设计师,我从来没有想过在预见中需要这种行为。c++能够对对象进行切片,从而将不同大小的对象存储在一个向量中,这已经让我兴奋不已了。不幸的是,当基类是抽象类时,它不能再这样做了,如下所述:为什么我们不能声明std::vector?

一般的好的解决方案似乎是使用指针。但是这(1)迫使我做内存管理(2)我需要改变接口和重新编码很多东西。例如,考虑一下我最初有一个类接口返回一个std::vector,现在它返回一个std::vector,所以我需要检查并更改foo的所有调用;如果我在写一个库,这是很烦人的,甚至是不可能的。

所以基本上,我认为,这是一个小的特性添加大的代码后果

我的问题是w.r.t.编码标准。我怎样才能避免这些烦恼的发生呢?我应该一直使用指针,并进行所有的内存管理吗?我是否应该总是假设一个类可能在此过程中变得抽象?

编辑,答案:根据402的答案,我做了这个片段:

#include <iostream>
#include <vector>
#include <memory>
struct foo {
    virtual void print() =0;
};
struct goo : public foo {
    int a;
    void print() { std::cout << "goo"; }
};
struct moo : public foo {
    int a,b;
    void print() { std::cout << "moo"; }
};
typedef std::unique_ptr<foo> foo_ptr;
int main() {
    std::vector<std::unique_ptr<foo> > foos;
    foos.push_back(foo_ptr(new moo));
    foos.push_back(foo_ptr(new goo));
    foos.push_back(foo_ptr(new goo));
    foos.push_back(foo_ptr(new moo));
    for(auto it = foos.begin(); it!=foos.end(); ++it) {
        it->get()->print();
    }
    return 0;
}

来源:http://ideone.com/ym4SY2

如果你的编译器支持c++ 11的特性,一个解决方案是使用std::vector<std::shared_ptr<foo>>std::vector<std::unique_ptr<foo>>代替原始指针,就像下面的例子:

#include <iostream>
#include <memory>
#include <vector>
struct foo {
    virtual void print() = 0;
};
struct goo : public foo {
    int a;
    void print() { std::cout << "goo"; }
};
struct moo : public foo {
    int a,b;
    void print() { std::cout << "moo"; }
};
auto main() -> int {
    std::vector<std::shared_ptr<foo>> v{std::make_shared<goo>(), std::make_shared<moo>()};
    for(auto i : v) { 
        i->print();
        std::cout << std::endl;
    }
    return 0;
}

或与std::vector<std::unique_ptr<foo>>:

auto main() -> int {
    std::vector<std::unique_ptr<foo>> v;
    v.push_back(std::move(std::unique_ptr<goo>(new goo)));
    v.push_back(std::move(std::unique_ptr<moo>(new moo)));
    for(auto it(v.begin()), ite(v.end()); it != ite; ++it) { 
        (*it)->print();
        std::cout << std::endl;
    }
    return 0;
}
因此,您不必担心内存释放。

您可以使用原始指针并正确处理内存

std::vector< AbstractBase*>

或者您可以使用智能指针,即std::shared_ptr(通过指针保留对象的共享所有权的智能指针)或std::unique_ptr(通过指针保留对象的唯一所有权的智能指针,并在unique_ptr超出作用域时销毁该对象),让库为您做内存管理。最后你会得到像

这样的东西
std::vector< std::shared_ptr<AbstractBase>>

std::vector< std::unique_ptr<AbstractBase>>

http://en.cppreference.com/w/cpp/memory/shared_ptrhttp://en.cppreference.com/w/cpp/memory/unique_ptr

我建议使用shared_ptr ie:

vector<shared_ptr<foo> > 

而不是原始指针。这将解决绝大多数内存管理问题。

第二个问题仍然存在,因为你需要在某些方面重新设计你的界面。但是在处理抽象基类时需要指针,因此对此您无能为力。如果foo是抽象的,就不能直接访问它。如果可以的话,设计你的界面,使其隐藏这些细节。

对不起,这可能不是你想要的答案,但这是我最好的建议。

您可以包装类的多态关系并使用智能指针:

#include <iostream>
#include <memory>
#include <vector>
class Base
{
    protected:
    struct Implementation
    {
        virtual ~Implementation() {}
        virtual void print() const = 0;
    };
    Implementation& self() const { return *m_self; }
    protected:
    Base(std::shared_ptr<Implementation> self)
    :   m_self(self)
    {}
    public:
    void print() const { self().print(); }
    private:
    std::shared_ptr<Implementation> m_self;
};
class Foo : public Base
{
    protected:
    struct Implementation : Base::Implementation
    {
        virtual void print() const { std::cout << "Foon"; }
    };
    Implementation& self() const { return static_cast<Implementation&>(Base::self()); }
    public:
    Foo() : Base(std::make_shared<Implementation>()) {}
};
class Goo : public Base
{
    protected:
    struct Implementation : Base::Implementation
    {
        virtual void print() const { std::cout << "Goon"; }
    };
    Implementation& self() const { return static_cast<Implementation&>(Base::self()); }
    public:
    Goo() : Base(std::make_shared<Implementation>()) {}
};
int main() {
    std::vector<Base> v = { Foo(), Goo() };
    for(const auto& x: v)
        x.print();
}

如何在foo周围编写封装foo*并隐式转换为foo&的包装器?

使用copy语义,调用存储对象上的底层克隆来进行深度复制。这至少不比按值存储所有内容的初衷差。如果您最终将所有内容存储为指向抽象基的指针,那么这与unique_ptr具有相同的间接级别,但是可复制的(而unique_ptr则不是)。另一方面,这比shared_ptr的开销要小。

添加clone()到抽象层次:

struct foo {
    virtual void print() const = 0;
    virtual ~foo() {};
    virtual foo* clone() = 0;
};
struct goo : public foo {
    int a;
    void print() const { std::cout << "goo" << std::endl; }
    foo* clone() { return new goo(*this); }
};
struct moo : public foo {
    int a,b;
    void print() const { std::cout << "moo" << std::endl; }
    foo* clone() { return new moo(*this); }
};

foo周围定义foo_w包装器,参见复制-交换习惯用法。

struct foo_w {
    foo_w(foo *f = nullptr) : fp(f) {}
    ~foo_w() { delete fp; }
    foo_w(const foo_w& that) : fp(that.fp->clone()) {}
    foo_w(foo_w&& that) : foo_w() { swap(*this, that); }
    foo_w& operator=(foo_w rhs) {
       swap(*this, rhs);
       return *this;
    }
    friend void swap(foo_w& f, foo_w& s) {
       using std::swap;
       swap(f.fp, s.fp);
    }
    operator foo&() { return *fp; } 
    operator const foo&() const { return *fp; } 
    foo& get() { return *fp; }
    const foo& get() const { return *fp; }
    // if we rewrite interface functions here
    // calls to get() could be eliminated (see below)
    // void print() { fp->print(); };
private:
    foo *fp;
};

用法如下:

#include <iostream>
#include <memory>
#include <vector>
// class definitions here...
int main() {
    std::vector<foo_w> foos;
    foos.emplace_back(new moo);
    foos.emplace_back(new goo);
    foos.emplace_back(new goo);
    foos.emplace_back(new moo);
    // variant 1: do it through a getter:
    for(auto it = foos.begin(); it!=foos.end(); ++it) {
        it->get().print();
        // the presence of a proxy is almost hidden
        // if we redefine interface in foo_w
        // it->print();
    }
    // variant 2: use it through reference to foo
    for(auto it = foos.begin(); it!=foos.end(); ++it) {
        foo& fr = *it;
        fr.print();
    }
    // variant 3: looks really nice with range-for
    for(foo& fr : foos)
        fr.print();
    return 0;
}

包装器的行为完全取决于您的需要。也许如果你同意unique_ptr不可复制,那是一个更好的方法,对我来说这是至关重要的,所以我最终得到了这个。还可以看看std::reference_wrapper如何在容器中存储类似引用的对象。

最新更新