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


79

도메인 별 개체의 컨테이너를 모델링하기 위해 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.
}

6
오 굿! 내 블로그를 punchlet.wordpress.com에 게시 할 수있는 또 다른 기회입니다. 기본적으로 무료 함수를 작성하고 "더 많은 OO"래퍼 접근 방식을 잊어 버립니다. 더 이상 OO가 아닙니다. 상속을 사용한다면이 경우에는 사용하지 말아야합니다. OO! = 클래스를 기억하십시오.

1
@Neil :하지만 .. 전역 함수는 사악합니다 !!! 모든 것이 대상입니다! ;)
Emile Cormier

4
네임 스페이스에 넣으면 전역 적이 지 않습니다.

1
벡터의 전체 인터페이스를 실제로 노출하려면 C ++에서 컴포지션을 사용하고 getter (const 및 non-const 버전)를 통해 벡터에 대한 참조를 노출하는 것이 더 좋습니다. Java에서는 상속 할 뿐이지 만 Java에서는 문서를 무시하고 잘못된 포인터를 통해 개체를 삭제 (또는 다시 상속하고 엉망으로 만드는) 한 다음 불평을합니다. 제한된 청중을 위해 아마도 사용자가 동적 다형성을 좋아하거나 최근에 자바 프로그래머라면 그들이 오해 할 것이라고 확신 할 수있는 인터페이스를 디자인하는 것입니다.
Steve Jessop

1
문서를 완전히 무시하는 사람들로부터 보호 할 수는 없습니다. 이러한 오용으로 인해 Java에서 C ++만큼이나 많은 문제가 발생한다는 사실에 놀라지 않습니다.

답변:


75

위험은 기본 클래스 ( 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로 변경할 수 있습니다 .


35

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

2
나는 private상속이 여전히 상속이고 따라서 구성보다 더 강한 관계 라는 것을 언급 할 수 없다 . 특히, 클래스의 구현을 변경하면 반드시 바이너리 호환성이 깨질 것입니다.
Matthieu M.

8
개인 상속과 개인 데이터 멤버는 둘 다 변경 될 때 이진 호환성을 깨고, 친구 (몇 안되는)를 제외하고는 일반적으로 그들 사이를 전환하는 것이 어렵지 않습니다. 또한 "base-from-member 관용구"를 참조하십시오.

: 기본 -에서 - 회원 관용구 - 호기심에 대한 en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Base-from-Member
에밀 코미

1
@MatthieuM. ABI를 깨는 것은 대부분의 응용 프로그램에서 전혀 문제가되지 않습니다. 일부 라이브러리조차도 더 나은 성능을 위해 Pimpl없이 살고 있습니다.
문서화

15

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

어쨌든, 토론에 추가 할 것이 있습니다. 이전에 자체 템플릿 전문화를 만든 방법은 기본 클래스로 사용할 다음과 같은 클래스를 정의하는 것입니다.

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

이러한 클래스는 STL 컨테이너와 동일한 인터페이스를 노출합니다. 수정 작업과 수정하지 않는 작업을 별개의 기본 클래스로 분리하는 효과가 마음에 들었습니다. 이것은 const-correctness에 정말 좋은 영향을 미칩니다. 한 가지 단점은 연관 컨테이너와 함께 사용하려면 인터페이스를 확장해야한다는 것입니다. 그래도 필요하지 않았습니다.


좋은! 나는 그것을 사용할 수 있습니다. 그러나 다른 사람들은 컨테이너를 개조하는 아이디어에 대해 다시 생각 했으므로 아마 사용하지 않을 것입니다. :)
Emile Cormier

즉, 무거운 템플릿 프로그래밍은 나쁜 스파게티 코드, 방대한 라이브러리, 기능의 열악한 격리 및 이해할 수없는 컴파일 시간 오류와 같은 모든 비트로 이어질 수 있습니다.
Erik Aronesty

5

이 경우 상속은 나쁜 생각입니다. STL 컨테이너에는 가상 소멸자가 없으므로 메모리 누수가 발생할 수 있습니다.

일부 기능 만 추가해야하는 경우 전역 메서드 또는 컨테이너 멤버 포인터 / 참조가있는 경량 클래스에서 선언 할 수 있습니다. 이 오프 코스에서는 메소드를 숨기는 것을 허용하지 않습니다. 이것이 실제로 당신이 추구하는 것이라면, 전체 구현을 재 선언하는 다른 옵션이 없습니다.


헤더에서 메서드를 선언하지 않고 대신 구현에서만 메서드를 숨길 수 있습니다. 더미 클래스에서 비공개 정적 메서드를 만들어 (우선 관계를 제공 할 수 있으며, 이는 헤더 전용이어야하는 템플릿에서 작동합니다.) ) 또는 "세부 사항"또는 유사한 이름의 네임 스페이스에 넣습니다. (세 가지 모두 기존의 개인 방법과 동일하게 작동합니다.)

헤더에 선언하지 않음으로써 '벡터'의 방법을 숨길 수 있다고 생각하는 방법을 이해하지 못했습니다. 이미 벡터로 선언되었습니다.
Jherico

Jherico : 나 한테 말하는거야 아니면 stijn? 어느 쪽이든, 나는 당신이 우리 중 한 명을 오해했다고 생각합니다.

@roger 나는 Jherico를 두 번째로했고 내가 당신을 이해한다고 생각하지 않는다 : std :: vector 또는 다른 것에서 메서드를 숨기는 것에 대해 이야기하고 있습니까? 또한 메서드를 다른 네임 스페이스에두면 어떻게 숨겨 질까요? 누구나 액세스 할 수있는 헤더에 선언되어있는 한 private 키워드가 숨기는 방식으로 실제로 숨겨지지 않습니까?
stijn 2010 년

stijn : 그것이 제가 개인 접근에 대해 지적한 것입니다. 헤더에 접근 할 수있는 사람은 누구나 소스를 읽거나 -Dprivate=public컴파일러 명령 줄에서 사용할 수 있기 때문에 실제로 숨겨져 있지 않습니다 . private과 같은 액세스 지정자는 대부분 문서이며 강제 적용됩니다.

4

가상 dtor를 제외하고 상속할지 포함할지 결정하는 것은 생성중인 클래스를 기반으로하는 디자인 결정이어야합니다. 당신은 결코 상속 컨테이너 기능을해야 단지 때문에 쉽게 용기를 포함하고 단순한 래퍼처럼 보일 몇 가지 추가 및 제거 기능을 추가하는 것보다 하지 않는 한 당신은 확실히 당신이 만들고있는 클래스는 일종의-의 용기라고 말할 수 있습니다. 예를 들어, 교실 수업은 종종 학생 객체를 포함하지만 교실은 대부분의 목적에서 일종의 학생 목록이 아니므로 목록에서 상속해서는 안됩니다.


1

다음과 같이하는 것이 더 쉽습니다.

typedef std::vector<MyObject> MyContainer;

3
이해 합니다만, typedef (std :: vector <MuObject> + mods) MyContainer consisely.
Emile Cormier

1

전달 방법은 어쨌든 인라인됩니다. 이렇게하면 더 나은 성능을 얻을 수 없습니다. 실제로 성능이 저하 될 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.