구현을 누설하지 않고 내부 벡터의 반복을 허용


32

사람들의 목록을 나타내는 수업이 있습니다.

class AddressBook
{
public:
  AddressBook();

private:
  std::vector<People> people;
}

고객이 사람들의 벡터를 반복하도록하고 싶습니다. 내가 가진 첫 번째 생각은 간단했습니다.

std::vector<People> & getPeople { return people; }

그러나 구현 세부 정보를 클라이언트에 유출하고 싶지 않습니다 . 벡터가 수정 될 때 특정 불변을 유지하고 싶을 수 있으며 구현을 누출하면 이러한 불변에 대한 제어를 잃습니다.

내부를 누설하지 않고 반복을 허용하는 가장 좋은 방법은 무엇입니까?


2
우선, 제어를 유지하려면 벡터를 const 참조로 반환해야합니다. 여전히 구현 세부 사항을 그런 식으로 노출 할 것이므로 클래스를 반복 가능하게 만들고 데이터 구조를 노출시키지 않는 것이 좋습니다 (내일 해시 테이블이 될 수 있습니까?).
idoby

빠른 구글 검색으로 나에게이 예가 드러났다 : sourcemaking.com/design_patterns/Iterator/cpp/1
Doc Brown

1
@DocBrown이 말한 것은 적절한 해결책 일 것입니다. 실제로 이것은 AddressBook 클래스에 begin () 및 end () 메소드 (const 과부하 및 결국 cbegin / cend)를 제공하여 벡터의 begin () 및 end ( ). 이렇게하면 대부분의 표준 알고리즘에서 수업을 사용할 수 있습니다.
stijn

1
@stijn 댓글이 아니라 답이 될 것입니다 :-)
Philip Kendall

1
@stijn 아니요, DocBrown과 링크 된 기사가 아닙니다. 올바른 해결책은 컨테이너 클래스를 가리키는 프록시 클래스와 위치를 표시하는 안전한 메커니즘을 사용하는 것입니다. 벡터의 복귀 begin()end()(1) 이들 유형의 예로서 다른 컨테이너로 전환을 방지 벡터 반복자 (클래스)이기 때문에 위험하다 set. (2) 벡터가 수정되면 (예 : 성장 또는 일부 항목 삭제) 벡터 반복기의 일부 또는 전부가 무효화되었을 수 있습니다.
rwong

답변:


25

내부를 누설하지 않고 반복을 허용 하는 것은 반복기 패턴이 약속하는 것과 정확히 일치합니다. 물론 그것은 주로 이론이므로 실용적인 예는 다음과 같습니다.

class AddressBook
{
  using peoples_t = std::vector<People>;
public:
  using iterator = peoples_t::iterator;
  using const_iterator = peoples_t::const_iterator;

  AddressBook();

  iterator begin() { return people.begin(); }
  iterator end() { return people.end(); }
  const_iterator begin() const { return people.begin(); }
  const_iterator end() const { return people.end(); }
  const_iterator cbegin() const { return people.cbegin(); }
  const_iterator cend() const { return people.cend(); }

private:
  peoples_t people;
};

STL의 시퀀스와 마찬가지로 표준 beginend메서드를 제공 하고 벡터의 메서드로 전달하여 간단하게 구현할 수 있습니다. 이것은 구현 세부 사항을 누설 합니다. 즉, 벡터 반복자를 반환하지만 정상적인 클라이언트는 그에 의존해서는 안되므로 걱정하지 않아도됩니다. 여기에 모든 과부하가 표시되었지만 클라이언트가 People 항목을 변경할 수없는 경우 const 버전을 제공하여 시작할 수 있습니다. 표준 이름 지정을 사용하면 이점이 있습니다. 코드를 읽는 사람은 코드가 '표준'반복을 제공한다는 것을 알고 있으며 모든 일반적인 알고리즘, 루프 기반 범위 등에서 작동합니다.


참고 :이 확실히 작동하고 허용됩니다 불구하고 그것의 가치가 질문의 rwong 의견의 메모를 복용 : 반복자의 기초가되는 실제의 클라이언트가 독립적으로 만들 것 여기에 추가 래퍼 / 프록시 주위 벡터의 반복자를 추가
스테인

또한, 노트가 제공하는 것을 begin()하고 end()단지 앞으로 벡터의 해당을 begin()하고하는 end()벡터 자체의 요소를 수정할 수있는 사용자 수, 아마도 사용 std::sort(). 보존하려는 불변량에 따라 허용 될 수도 있고 그렇지 않을 수도 있습니다. 제공 begin()하고 end(), 그래도 C ++ 11 루프 기반 범위를 지원하는 것이 필요하다.
Patrick Niedzielski

C ++ 14를 사용할 때 auto를 사용하여 리턴 유형의 반복자 함수와 동일한 코드를 표시해야합니다.
Klaim

이것이 구현 세부 사항을 어떻게 숨기고 있습니까?
BЈовић

@ BЈовић 완전한 벡터를 노출 시키지 않음 - 숨겨져 있다고해서 반드시 구현이 헤더에서 문자 그대로 숨겨져 소스 파일에 넣어야한다는 것을 의미하지는 않습니다. 사설 클라이언트라면 어쨌든 액세스 할 수없는 경우
stijn

4

반복이 필요한 모든 것이라면 아마도 래퍼로 std::for_each충분할 것입니다.

class AddressBook
{
public:
  AddressBook();

  template <class F>
  void for_each(F f) const
  {
    std::for_each(begin(people), end(people), f);
  }

private:
  std::vector<People> people;
};

cbegin / cend를 사용하여 const 반복을 시행하는 것이 좋습니다. 그러나이 솔루션은 기본 컨테이너에 액세스하는 것보다 훨씬 낫습니다.
galop1n

galop1n @는 않습니다 시행 const반복합니다. 는 for_each()A는 const부재 기능. 따라서 멤버 people는로 표시됩니다 const. 따라서, begin()end()같은 과부하됩니다 const. 따라서, 그들은 반환 const_iterator에들 people. 따라서을 f()받게됩니다 People const&. 글쓰기 cbegin()/ cend()여기에 실제로는 아무것도 변하지 않을 것입니다.하지만 강박적인 사용자는 const여전히 가치가 있다고 주장 할 수 있습니다. 그것은 최소한으로, 무슨 뜻인지 말처럼 단 2 문자, (b)는 I를의 const(c)는이 실수로 어딘가에 비 붙여에 대해 경비, const등,
underscore_d

3

pimpl idiom 을 사용하고 컨테이너를 반복하는 메소드를 제공 할 수 있습니다 .

헤더에서 :

typedef People* PeopleIt;

class AddressBook
{
public:
  AddressBook();


  PeopleIt begin();
  PeopleIt begin() const;
  PeopleIt end();
  PeopleIt end() const;

private:
  struct Imp;
  std::unique_ptr<Imp> pimpl;
};

소스에서 :

struct AddressBook::Imp
{
  std::vector<People> people;
};

PeopleIt AddressBook::begin()
{
  return &pimpl->people[0];
}

이렇게하면 클라이언트가 헤더의 typedef를 사용하는 경우 사용중인 컨테이너 종류를 알 수 없습니다. 그리고 구현 세부 사항은 완전히 숨겨져 있습니다.


1
이것은 올바른 구현입니다. 완전한 구현은 숨기고 추가 오버 헤드는 없습니다.
추상화가 전부입니다.

2
@Abstractioniseverything. " 추가 오버 헤드 없음 "은 명백히 거짓입니다. PImpl은 모든 인스턴스에 대해 동적 메모리 할당 (및 나중에 사용 가능)과이를 통과하는 모든 메소드에 대해 포인터 간접 (최소 1)을 추가합니다. 주어진 상황에서 오버 헤드 가 많은지 여부는 벤치마킹 / 프로파일 링에 달려 있으며 많은 경우 아마도 완벽하게 괜찮을 것입니다. 그러나 오버 헤드 가 없다고 선언하는 것은 절대적으로 사실이 아니며 무책임하다고 생각 합니다.
underscore_d

@underscore_d 동의합니다. 거기에 무책임하다는 의미는 아니지만, 나는 그 문맥에 먹이가 된 것 같아요. "추가 비용이 발생하지 않습니다 ..."는 기술적으로 부정확합니다. 사과 ...
추상화가 전부입니다.

1

멤버 함수를 제공 할 수 있습니다.

size_t Count() const
People& Get(size_t i)

구현 세부 사항을 노출하지 않고 액세스 할 수 있으며 (예 : 연속성) 반복자 클래스 내에서 사용

class Iterator
{
    AddressBook* addressBook_;
    size_t index_;

public:
    Iterator(AddressBook& addressBook, size_t index=0) 
    : addressBook_(&addressBook), index_(index) {}

    People& operator*()
    {
        return addressBook_->Get(index_);
    }

    Iterator& operator ++ ()
    {
       ++index_;
       return *this;
    }

    bool operator != (const Iterator& i) const
    {
        assert(addressBook_ == i.addressBook_);
        return index_ != i.index_;
    }
};

그런 다음 주소록에서 반복자를 다음과 같이 리턴 할 수 있습니다.

AddressBook::Iterator AddressBook::begin()
{
    return Iterator(this);
}

AddressBook::Iterator AddressBook::end()
{
    return Iterator(this, Count());
}

아마도 반복자 클래스를 특성 등으로 살려야 할 것입니다. 그러나 이것이 당신이 요청한 것을 할 것이라고 생각합니다.


1

std :: vector에서 함수를 정확하게 구현하려면 아래와 같이 개인 상속을 사용하고 노출되는 것을 제어하십시오.

template <typename T>
class myvec : private std::vector<T>
{
public:
    using std::vector<T>::begin;
    using std::vector<T>::end;
    using std::vector<T>::push_back;
};

편집 : 내부 데이터 구조 (예 : std :: vector)를 숨기려면 권장하지 않습니다.


이러한 상황에서 상속 자주 (혼란과 불편, (당신이 구성을 사용하고 여기에 전달할 이렇게 몇몇이있다, 특히 이후 방법을 전달 제공해야한다) 최고의 매우 게으른 당신이 당신의 자신의 방법을 추가 할 경우 그와 충돌하는 vector것, 당신이 사용하고 싶지는 않지만 상속해야합니까?) 그리고 아마도 위험 할 수도 있습니다. 그런 포인터를 통해 파생 된 obj, 단순히 그것을 파괴하는 것은 UB?)
underscore_d
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.