programing

위임이 아닌 STL 컨테이너에서 구현을 상속해도 괜찮습니까?

nasanasas 2020. 10. 24. 10:21
반응형

위임이 아닌 STL 컨테이너에서 구현을 상속해도 괜찮습니까?


도메인 별 개체의 컨테이너를 모델링하기 위해 std :: vector를 적용하는 클래스가 있습니다. 대부분의 std :: vector API를 사용자에게 노출하여 사용자가 컨테이너에서 익숙한 방법 (size, clear, at 등)과 표준 알고리즘을 사용할 수 있도록하고 싶습니다. 이것은 내 디자인에서 반복되는 패턴 인 것 같습니다.

class MyContainer : public std::vector<MyObject>
{
public:
   // Redeclare all container traits: value_type, iterator, etc...

   // Domain-specific constructors
   // (more useful to the user than std::vector ones...)

   // Add a few domain-specific helper methods...

   // Perhaps modify or hide a few methods (domain-related)
};

구현을 위해 클래스를 재사용 할 때 상속보다 컴포지션을 선호하는 관행을 알고 있지만 한계가 있습니다! 내가 모든 것을 std :: vector에 위임한다면, 32 개의 포워딩 함수가있을 것입니다!

그래서 내 질문은 ... 그런 경우에 구현을 상속하는 것이 정말 나쁜가요? 위험은 무엇입니까? 너무 많은 타이핑없이 이것을 구현할 수있는 더 안전한 방법이 있습니까? 구현 상속을 사용하는 이단자입니까? :)

편집하다:

사용자가 std :: vector <> 포인터를 통해 MyContainer를 사용해서는 안된다는 점을 명확히하는 것은 어떻습니까?

// non_api_header_file.h
namespace detail
{
   typedef std::vector<MyObject> MyObjectBase;
}

// api_header_file.h
class MyContainer : public detail::MyObjectBase
{
   // ...
};

부스트 라이브러리는 항상 이런 일을하는 것 같습니다.

편집 2 :

제안 중 하나는 무료 기능을 사용하는 것입니다. 여기에 의사 코드로 표시하겠습니다.

typedef std::vector<MyObject> MyCollection;
void specialCollectionInitializer(MyCollection& c, arguments...);
result specialCollectionFunction(const MyCollection& c);
etc...

더 많은 OO 방법 :

typedef std::vector<MyObject> MyCollection;
class MyCollectionWrapper
{
public:
   // Constructor
   MyCollectionWrapper(arguments...) {construct coll_}

   // Access collection directly
   MyCollection& collection() {return coll_;} 
   const MyCollection& collection() const {return coll_;}

   // Special domain-related methods
   result mySpecialMethod(arguments...);

private:
   MyCollection coll_;
   // Other domain-specific member variables used
   // in conjunction with the collection.
}

위험은 기본 클래스 ( delete , delete [] 및 잠재적으로 다른 할당 해제 메서드)에 대한 포인터를 통해 할당 해제됩니다 . 이러한 클래스 ( deque , map , string 등)에는 가상 dtor가 없기 때문에 해당 클래스에 대한 포인터만으로 제대로 정리하는 것은 불가능합니다.

struct BadExample : vector<int> {};
int main() {
  vector<int>* p = new BadExample();
  delete p; // this is Undefined Behavior
  return 0;
}

즉, 실수로이 작업을하지 않으려는 경우 상속하는 데 큰 단점이 거의 없지만 어떤 경우에는 큰 문제가됩니다. 다른 단점으로는 구현 세부 사항 및 확장 (일부는 예약 된 식별자를 사용하지 않을 수 있음)과 충돌하고 비대해진 인터페이스 ( 특히 문자열 ) 처리가 있습니다. 그러나 스택 과 같은 컨테이너 어댑터 에는 보호 된 멤버 c (적응하는 기본 컨테이너)가 있고 파생 클래스 인스턴스에서만 액세스 할 수 있기 때문에 상속이 의도 된 경우도 있습니다.

상속 또는 구성 대신 반복기 쌍 또는 컨테이너 참조를 가져 와서 작동 하는 자유 함수작성 하는 것이 좋습니다. 실제로 모든 <algorithm>이 이에 대한 예입니다. make_heap는 , pop_heappush_heap는 특히 대신 도메인 특정 컨테이너의 멤버 함수를 사용한 예이다.

따라서 데이터 유형에 컨테이너 클래스를 사용하고 도메인 별 로직에 대해 무료 함수를 호출하십시오. 그러나 typedef를 사용하여 모듈화를 수행 할 수 있습니다.이를 통해 선언을 단순화하고 일부를 변경해야하는 경우 단일 지점을 제공 할 수 있습니다.

typedef std::deque<int, MyAllocator> Example;
// ...
Example c (42);
example_algorithm(c);
example_algorithm2(c.begin() + 5, c.end() - 5);
Example::iterator i; // nested types are especially easier

value_type 및 할당자는 typedef를 사용하여 이후 코드에 영향을주지 않고 변경 될 수 있으며 컨테이너조차도 deque 에서 vector로 변경할 수 있습니다 .


private 상속과 'using'키워드를 결합하여 위에서 언급 한 대부분의 문제를 해결할 수 있습니다. private 상속은 'is-implemented-in-terms-of'이며 private이기 때문에 기본 클래스에 대한 포인터를 보유 할 수 없습니다.

#include <string>
#include <iostream>

class MyString : private std::string
{
public:
    MyString(std::string s) : std::string(s) {}
    using std::string::size;
    std::string fooMe(){ return std::string("Foo: ") + *this; }
};

int main()
{
    MyString s("Hi");
    std::cout << "MyString.size(): " << s.size() << std::endl;
    std::cout << "MyString.fooMe(): " << s.fooMe() << std::endl;
}

모두가 이미 언급했듯이 STL 컨테이너에는 가상 소멸자가 없으므로 상속하는 것은 기껏해야 안전하지 않습니다. 나는 항상 템플릿을 사용하는 일반적인 프로그래밍을 상속이없는 OO의 다른 스타일로 간주했습니다. 알고리즘은 필요한 인터페이스를 정의합니다. 정적 인 언어에서 얻을 수있는 것처럼 Duck Typing에 가깝습니다 .

Anyway, I do have something to add to the discussion. The way that I have created my own template specializations previously is to define classes like the following to use as base classes.

template <typename Container>
class readonly_container_facade {
public:
    typedef typename Container::size_type size_type;
    typedef typename Container::const_iterator const_iterator;

    virtual ~readonly_container_facade() {}
    inline bool empty() const { return container.empty(); }
    inline const_iterator begin() const { return container.begin(); }
    inline const_iterator end() const { return container.end(); }
    inline size_type size() const { return container.size(); }
protected: // hide to force inherited usage only
    readonly_container_facade() {}
protected: // hide assignment by default
    readonly_container_facade(readonly_container_facade const& other):
        : container(other.container) {}
    readonly_container_facade& operator=(readonly_container_facade& other) {
        container = other.container;
        return *this;
    }
protected:
    Container container;
};

template <typename Container>
class writable_container_facade: public readable_container_facade<Container> {
public:
    typedef typename Container::iterator iterator;
    writable_container_facade(writable_container_facade& other)
        readonly_container_facade(other) {}
    virtual ~writable_container_facade() {}
    inline iterator begin() { return container.begin(); }
    inline iterator end() { return container.end(); }
    writable_container_facade& operator=(writable_container_facade& other) {
        readable_container_facade<Container>::operator=(other);
        return *this;
    }
};

These classes expose the same interface as an STL container. I did like the effect of separating the modifying and non-modifying operations into distinct base classes. This has a really nice effect on const-correctness. The one downside is that you have to extend the interface if you want to use these with associative containers. I haven't run into the need though.


In this case, inheriting is a bad idea: the STL containers do not have virtual destructors so you might run into memory leaks (plus, it's an indication that STL containers are not meant to be inherited in the first place).

If you just need to add some functionality, you can declare it in global methods, or a lightweight class with a container member pointer/reference. This off course doesn't allow you to hide methods: if that is really what you are after, then there's no other option then redeclaring the entire implementation.


Virtual dtors aside, the decision to inherit versus contain should be a design decision based the class you are creating. You should never inherit container functionality just because its easier than containing a container and adding a few add and remove functions that seem like simplistic wrappers unless you can definitively say that the class you are creating is a kind-of the container. For instance, a classroom class will often contain student objects, but a classroom isn't a kind of list of students for most purposes, so you shouldn't be inheriting from list.


It is easier to do:

typedef std::vector<MyObject> MyContainer;

The forwarding methods will be inlined away, anyhow. You will not get better performance this way. In fact, you will likely get worse performance.

참고URL : https://stackoverflow.com/questions/2034916/is-it-okay-to-inherit-implementation-from-stl-containers-rather-than-delegate

반응형