C++에서 값 의미를 갖는 다형성 컨테이너를 가질 수 있습니까?

StackOverflow https://stackoverflow.com/questions/41045

  •  09-06-2019
  •  | 
  •  

문제

일반적으로 나는 C++에서 포인터 의미론보다는 값을 사용하는 것을 선호합니다. vector<Class> 대신에 vector<Class*>).일반적으로 약간의 성능 손실은 동적으로 할당된 개체를 삭제하는 것을 기억하지 않아도 되기 때문에 상쇄됩니다.

불행하게도 공통 기반에서 파생되는 다양한 개체 유형을 저장하려는 경우 값 컬렉션이 작동하지 않습니다.아래 예를 참조하세요.

#include <iostream>

using namespace std;

class Parent
{
    public:
        Parent() : parent_mem(1) {}
        virtual void write() { cout << "Parent: " << parent_mem << endl; }
        int parent_mem;
};

class Child : public Parent
{
    public:
        Child() : child_mem(2) { parent_mem = 2; }
        void write() { cout << "Child: " << parent_mem << ", " << child_mem << endl; }

        int child_mem;
};

int main(int, char**)
{
    // I can have a polymorphic container with pointer semantics
    vector<Parent*> pointerVec;

    pointerVec.push_back(new Parent());
    pointerVec.push_back(new Child());

    pointerVec[0]->write(); 
    pointerVec[1]->write(); 

    // Output:
    //
    // Parent: 1
    // Child: 2, 2

    // But I can't do it with value semantics

    vector<Parent> valueVec;

    valueVec.push_back(Parent());
    valueVec.push_back(Child());    // gets turned into a Parent object :(

    valueVec[0].write();    
    valueVec[1].write();    

    // Output:
    // 
    // Parent: 1
    // Parent: 2

}

내 질문은 다음과 같습니다케이크를 먹고(가치 의미) 그것을 먹을 수도 있습니까(다형성 컨테이너)?아니면 포인터를 사용해야 합니까?

도움이 되었습니까?

해결책

서로 다른 클래스의 객체는 크기가 다르기 때문에 이를 값으로 저장하면 결국 슬라이싱 문제가 발생하게 됩니다.

합리적인 해결책 중 하나는 컨테이너에 안전한 스마트 포인터를 저장하는 것입니다.나는 일반적으로 컨테이너에 저장하기에 안전한 Boost::shared_ptr을 사용합니다.std::auto_ptr은 그렇지 않습니다.

vector<shared_ptr<Parent>> vec;
vec.push_back(shared_ptr<Parent>(new Child()));

shared_ptr은 참조 계산을 사용하므로 모든 참조가 제거될 때까지 기본 인스턴스를 삭제하지 않습니다.

다른 팁

그래 넌 할수있어.

Boost.ptr_container 라이브러리는 표준 컨테이너의 다형성 값 의미 버전을 제공합니다.힙 할당 개체에 대한 포인터만 전달하면 컨테이너가 소유권을 갖게 되며 모든 추가 작업은 스마트 포인터를 사용하여 값 의미 체계의 거의 모든 이점을 제공하는 소유권 회수를 제외하고 값 의미 체계를 제공합니다. .

나는 단지 벡터<Foo>가 일반적으로 벡터<Foo*>보다 더 효율적이라는 점을 지적하고 싶었습니다.벡터<Foo>에서는 모든 Foo가 메모리에서 서로 인접해 있습니다.콜드 TLB 및 캐시를 가정하면 첫 번째 읽기는 페이지를 TLB에 추가하고 벡터 덩어리를 L# 캐시로 가져옵니다.후속 읽기에서는 웜 캐시와 로드된 TLB를 사용하며 가끔 캐시가 누락되고 TLB 오류가 덜 자주 발생합니다.

이를 벡터<Foo*>와 대조해 보세요.벡터를 채울 때 메모리 할당자로부터 Foo*를 얻습니다.할당자가 매우 똑똑하지 않거나(tcmalloc?) 시간이 지남에 따라 벡터를 천천히 채운다고 가정하면 각 Foo의 위치는 다른 Foos와 멀리 떨어져 있을 가능성이 높습니다.아마도 수백 바이트 정도일 수도 있고 메가바이트 정도 차이일 수도 있습니다.

최악의 경우 벡터<Foo*>를 스캔하고 각 포인터를 역참조하면 TLB 오류와 캐시 미스가 발생하게 됩니다. 많은 벡터<Foo>가 있는 경우보다 느립니다.(글쎄, 최악의 경우 각 Foo는 디스크로 페이지 아웃되었으며 모든 읽기에는 페이지를 다시 RAM으로 이동하기 위해 디스크 탐색() 및 read()가 발생합니다.)

따라서 필요할 때마다 계속해서 vector<Foo>를 사용하세요.:-)

대부분의 컨테이너 유형은 연결된 목록, 벡터, 트리 기반 등 특정 스토리지 전략을 추상화하려고 합니다.이러한 이유로 앞서 언급한 케이크를 소유하고 소비하는 데 어려움을 겪게 될 것입니다(예: 케이크는 거짓말입니다(주의:누군가가이 농담을해야했습니다)).

그래서 뭐 할까?몇 가지 귀여운 옵션이 있지만 대부분은 몇 가지 테마 또는 테마 조합 중 하나에 대한 변형으로 축소됩니다.적절한 스마트 포인터를 선택하거나 발명하고, 템플릿 또는 템플릿 템플릿을 영리한 방식으로 사용하고, 컨테이너별 이중 디스패치를 ​​구현하기 위한 후크를 제공하는 컨테이너용 공통 인터페이스를 사용합니다.

명시된 두 가지 목표 사이에는 기본적인 긴장감이 있으므로 원하는 것을 결정한 다음 기본적으로 원하는 것을 얻을 수 있는 것을 디자인하려고 노력해야 합니다.그것 ~이다 충분히 영리한 참조 카운팅과 영리한 팩토리 구현을 통해 포인터가 값처럼 보이도록 멋지고 예상치 못한 트릭을 수행하는 것이 가능합니다.기본 아이디어는 참조 카운팅, 요청 시 복사 및 불변성을 사용하고 (인자에 대해) 전처리기, 템플릿 및 C++의 정적 초기화 규칙의 조합을 사용하여 포인터 변환 자동화에 대해 가능한 한 스마트한 것을 얻는 것입니다.

나는 과거에 가상 프록시/봉투 문자/참조 계산 포인터를 사용하는 귀여운 트릭을 사용하여 C++에서 값 의미 체계 프로그래밍의 기초와 같은 작업을 수행하는 방법을 구상하는 데 시간을 보냈습니다.

그리고 나는 그것이 가능하다고 생각하지만 C++ 내에서 상당히 폐쇄적이고 C# 관리 코드와 유사한 세계를 제공해야 합니다(필요할 때 기본 C++로 돌파할 수 있는 것임).그래서 나는 당신의 생각에 많은 공감을 가지고 있습니다.

당신은 또한 고려할 수 있습니다 부스트::아무거나.이기종 컨테이너에 사용했습니다.값을 다시 읽을 때 any_cast를 수행해야 합니다.실패하면 bad_any_cast가 발생합니다.그럴 경우에는 잡아서 다음 유형으로 넘어갈 수 있습니다.

믿다 파생 클래스를 기본으로 any_cast하려고 하면 bad_any_cast가 발생합니다.나는 그것을 시도했다:

  // But you sort of can do it with boost::any.

  vector<any> valueVec;

  valueVec.push_back(any(Parent()));
  valueVec.push_back(any(Child()));        // remains a Child, wrapped in an Any.

  Parent p = any_cast<Parent>(valueVec[0]);
  Child c = any_cast<Child>(valueVec[1]);
  p.write();
  c.write();

  // Output:
  //
  // Parent: 1
  // Child: 2, 2

  // Now try casting the child as a parent.
  try {
      Parent p2 = any_cast<Parent>(valueVec[1]);
      p2.write();
  }
  catch (const boost::bad_any_cast &e)
  {
      cout << e.what() << endl;
  }

  // Output:
  // boost::bad_any_cast: failed conversion using boost::any_cast

말하자면, 나는 또한 shared_ptr 경로를 먼저 갈 것입니다!그냥 이게 좀 흥미로울 거라고 생각했어요.

보세요 static_cast 그리고 재해석_캐스트
C++ 프로그래밍 언어 3판에서 Bjarne Stroustrup이 130페이지에 설명합니다.이에 대한 전체 섹션은 6장에 있습니다.
Parent 클래스를 Child 클래스로 다시 캐스팅할 수 있습니다.이를 위해서는 각 항목이 언제인지 알아야 합니다.책에서는 Dr.Stroustrup은 이러한 상황을 피하기 위한 다양한 기술에 대해 이야기합니다.

이렇게 하지 마십시오.이는 처음에 달성하려는 다형성을 무효화합니다!

모든 것에 한 가지만 추가하면 1800 정보 이미 말했다.

당신은 살펴보고 싶을 수도 있습니다 "더 효과적인 C++" 작성자: Scott Mayers "항목 3:이 문제를 더 잘 이해하려면 배열을 다형성으로 처리하지 마십시오.

저는 값 유형 의미가 노출된 자체 템플릿 컬렉션 클래스를 사용하고 있지만 내부적으로는 포인터를 저장합니다.역참조될 때 포인터 대신 값 참조를 얻는 사용자 정의 반복자 클래스를 사용하고 있습니다.컬렉션을 복사하면 중복된 포인터 대신 깊은 항목 복사본이 만들어지며 여기에 대부분의 오버헤드가 있습니다(대신 얻는 것을 고려하면 아주 사소한 문제입니다).

그것은 귀하의 필요에 맞는 아이디어입니다.

이 문제에 대한 답을 찾는 동안 나는 이 두 가지를 모두 발견했습니다. 비슷한 질문.다른 질문에 대한 답변에는 두 가지 제안된 솔루션이 있습니다.

  1. std::Optional 또는 Boost::Optional과 방문자 패턴을 사용하세요.이 솔루션을 사용하면 새로운 유형을 추가하기는 어렵지만 새로운 기능을 추가하기는 쉽습니다.
  2. 다음과 비슷한 래퍼 클래스를 사용하세요. Sean Parent가 강연에서 발표합니다..이 솔루션을 사용하면 새로운 기능을 추가하기는 어렵지만 새로운 유형을 추가하는 것은 쉽습니다.

래퍼는 클래스에 필요한 인터페이스를 정의하고 그러한 객체 중 하나에 대한 포인터를 보유합니다.인터페이스 구현은 무료 기능을 사용하여 수행됩니다.

다음은 이 패턴의 구현 예입니다.

class Shape
{
public:
    template<typename T>
    Shape(T t)
        : container(std::make_shared<Model<T>>(std::move(t)))
    {}

    friend void draw(const Shape &shape)
    {
        shape.container->drawImpl();
    }
    // add more functions similar to draw() here if you wish
    // remember also to add a wrapper in the Concept and Model below

private:
    struct Concept
    {
        virtual ~Concept() = default;
        virtual void drawImpl() const = 0;
    };

    template<typename T>
    struct Model : public Concept
    {
        Model(T x) : m_data(move(x)) { }
        void drawImpl() const override
        {
            draw(m_data);
        }
        T m_data;
    };

    std::shared_ptr<const Concept> container;
};

그런 다음 다양한 모양이 일반 구조체/클래스로 구현됩니다.멤버 함수를 사용할지, 자유 함수를 사용할지 자유롭게 선택할 수 있습니다(그러나 멤버 함수를 사용하려면 위 구현을 업데이트해야 합니다).나는 무료 기능을 선호합니다:

struct Circle
{
    const double radius = 4.0;
};

struct Rectangle
{
    const double width = 2.0;
    const double height = 3.0;
};

void draw(const Circle &circle)
{
    cout << "Drew circle with radius " << circle.radius << endl;
}

void draw(const Rectangle &rectangle)
{
    cout << "Drew rectangle with width " << rectangle.width << endl;
}

이제 둘 다 추가할 수 있습니다. Circle 그리고 Rectangle 같은 것에 반대한다 std::vector<Shape>:

int main() {
    std::vector<Shape> shapes;
    shapes.emplace_back(Circle());
    shapes.emplace_back(Rectangle());
    for (const auto &shape : shapes) {
        draw(shape);
    }
    return 0;
}

이 패턴의 단점은 각 함수를 세 번 정의해야 하기 때문에 인터페이스에 많은 양의 상용구가 필요하다는 것입니다.장점은 복사 의미를 얻을 수 있다는 것입니다.

int main() {
    Shape a = Circle();
    Shape b = Rectangle();
    b = a;
    draw(a);
    draw(b);
    return 0;
}

이는 다음을 생성합니다.

Drew rectangle with width 2
Drew rectangle with width 2

걱정이 되신다면 shared_ptr, 다음으로 대체할 수 있습니다. unique_ptr.그러나 더 이상 복사할 수 없으므로 모든 개체를 이동하거나 수동으로 복사를 구현해야 합니다.Sean Parent는 그의 강연에서 이에 대해 자세히 논의했으며 위에서 언급한 답변에 구현이 나와 있습니다.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top