【问题标题】:abstract classes in std containersstd 容器中的抽象类
【发布时间】:2014-07-30 07:28:40
【问题描述】:

当我编程时,我经常使用多态性,因为它自然地对我需要的对象进行建模。另一方面,我经常使用标准容器来存储这些对象,并且我倾向于避免使用指针,因为这要么需要我释放对象而不是将它们从堆栈中弹出,要么需要我确定这些对象会继续存在我使用指针时的堆栈。当然,有各种各样的指针容器对象可以为您完成这项任务,但根据我的经验,它们也不理想,甚至令人讨厌。那是;如果存在这样一个简单的解决方案,它应该是 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,并且据我所知,不存在适当的替代方案,因此在从向量中将继承类的属性作为 foo 获取后,无法访问它们。

因此可以使用虚函数,但是这不允许分配 foo,因此不允许在向量中使用它们。见Why can't we declare a std::vector<AbstractClass>?

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

#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++ 能够对我的对象进行切片并因此将不同大小的对象存储在一个向量中,这让我感到非常激动。不幸的是,当基类是抽象的时,它不能再这样做了,如下所述:Why can't we declare a std::vector<AbstractClass>?

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

所以基本上,恕我直言,这是一个小功能添加大代码后果

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

编辑,答案:基于 40two 的回答,我制作了这个片段:

#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++ 能够对我的对象进行切片并因此将不同大小的对象存储在一个向量中这一事实而激动不已。”您似乎完全误解了切片是什么。切片是转换的(坏)名称。它占用了您的moo,并从中生成了foo不再是moo。 “切片”几乎总是一件的事情。向量存储不同大小的对象,它只存储foo个对象,它们都是相同大小的。
  • @Casey:它甚至无法编译,因为vector 无法使用抽象值类型进行实例化。链接代码显示“错误:无法分配抽象类型‘foo’的对象”
  • “切片”的工作方式如下:std::vector&lt;short&gt; v; v.push_back(1000000000);。发生什么了?该向量包含一个短整数,而不是一个整数。

标签: c++ c++11 virtual-functions


【解决方案1】:

如果您的编译器支持 C++11 功能,一个解决方案是使用 std::vector&lt;std::shared_ptr&lt;foo&gt;&gt;std::vector&lt;std::unique_ptr&lt;foo&gt;&gt; 而不是原始指针,如下例所示:

#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&lt;std::unique_ptr&lt;foo&gt;&gt;:

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

因此,您不必担心内存释放问题。

【讨论】:

  • 很公平,但它仍然改变了界面:) 虽然我担心我只能忍受这个,但我仍然想要一个可以帮助我的答案防止 问题。
  • @MooingDuck 你的意思是,你不能阻止这个问题?库开发者是如何做到的?
  • @Herbert:他们使用std::vector&lt;std::unique_ptr&lt;base&gt;&gt;
  • @MooingDuck 所以他们无论如何都会这样做,以防类变得抽象?
  • @Herbert:从技术上讲,你是 100% 正确的。在实践中,创建一个从非虚基派生的类是有问题的,而且实际上非常少见。如果从它公开继承是有意义的,那么基类有一个虚拟析构函数可能是一个好主意。如果可以合理地假设它可能会继承自,请使用std::vector&lt;std::unique_ptr&lt;base&gt;&gt;。是的,有时人们猜错了,必须进行转换,但这种情况很少见。 (另外,我认为你稍微误用了“抽象”这个词,但你已经足够接近它不会在这里造成问题。)
【解决方案2】:

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

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_ptr http://en.cppreference.com/w/cpp/memory/unique_ptr

【讨论】:

  • 我什至会说“更喜欢 value_ptr 而不是 unique_ptrshared_ptr”,尽管这是 C++ 社区中的一个争论点。
  • @Mankarse:就个人而言,我同意,但这更加主观和不标准。但是,我们都同意shared_ptr 在 99% 的情况下都是错误的工具。
  • @MooingDuck shared_ptr 之所以受到如此多的关注,是因为它试图成为“无所不能”的智能指针。我认为它作为一种福音派工具最有用,可以说服人们不使用newdelete 很容易。
【解决方案3】:

我建议使用 shared_ptr 即:

vector<shared_ptr<foo> > 

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

第二个问题仍然存在,因为您需要在某些区域重新设计界面。但是您对此无能为力,因为在使用抽象基类时需要指针。如果 foo 是抽象的,则不能仅将 foo 作为直接引用访问。如果可以,请设计您的界面,使其隐藏这些细节。

抱歉,这可能不是您要找的答案,但这是我最好的建议。

【讨论】:

    【解决方案4】:

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

    #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 << "Foo\n"; }
        };
    
        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 << "Goo\n"; }
        };
    
        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();
    }
    

    【讨论】:

    • 这有点冗长:S
    • 我喜欢这个想法,但我认为它不适合我的目标,因为就像我说的那样,它很冗长并且包含很多样板代码。不过谢谢!
    【解决方案5】:

    编写一个围绕foo 的包装器如何封装foo* 并隐式转换为foo&amp;

    它使用复制语义调用存储对象上的底层克隆来进行深度复制。这至少不比按价值存储一切的最初意图差。如果您最终将所有内容存储为指向抽象基的指针,那么它与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 包装器,参见copy-and-swap idiom

    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 在容器中存储类似引用的对象。

    【讨论】:

      猜你喜欢
      • 2021-12-18
      • 1970-01-01
      • 2021-12-31
      • 1970-01-01
      • 2019-10-23
      • 2016-03-12
      • 2018-02-06
      • 2014-02-02
      • 1970-01-01
      相关资源
      最近更新 更多