Cho phép lặp lại một vectơ nội bộ mà không rò rỉ việc thực hiện


32

Tôi có một lớp đại diện cho một danh sách những người.

class AddressBook
{
public:
  AddressBook();

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

Tôi muốn cho phép khách hàng lặp đi lặp lại trên vectơ của mọi người. Ý nghĩ đầu tiên tôi có chỉ đơn giản là:

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

Tuy nhiên, tôi không muốn rò rỉ các chi tiết triển khai cho khách hàng . Tôi có thể muốn duy trì một số bất biến nhất định khi vectơ được sửa đổi và tôi mất quyền kiểm soát các bất biến này khi tôi rò rỉ việc thực hiện.

Cách tốt nhất để cho phép lặp mà không rò rỉ nội bộ là gì?


2
Trước hết, nếu bạn muốn duy trì quyền kiểm soát, bạn nên trả về vectơ của bạn dưới dạng tham chiếu const. Bạn vẫn sẽ tiết lộ chi tiết triển khai theo cách đó, vì vậy tôi khuyên bạn nên làm cho lớp của mình có thể lặp lại và không bao giờ để lộ cấu trúc dữ liệu của bạn (có thể đó sẽ là bảng băm vào ngày mai?).
ngốc nghếch

Một tìm kiếm nhanh trên google đã tiết lộ cho tôi ví dụ này: sourcemaking.com/design_potypes/Iterator/cpp/1
Doc Brown

1
Những gì @DocBrown nói có khả năng là giải pháp phù hợp - trong thực tế, điều này có nghĩa là bạn cung cấp cho lớp Địa chỉ của bạn một phương thức start () và end () (cộng với quá tải const và cuối cùng là cbegin / cend) chỉ đơn giản trả về bắt đầu của vectơ () và kết thúc () ). Bằng cách làm như vậy, lớp của bạn cũng sẽ có thể được sử dụng bởi tất cả các đại số std.
stijn

1
@stijn Đó phải là một câu trả lời, không phải là một bình luận :-)
Philip Kendall

1
@stijn Không, đó không phải là những gì DocBrown và bài viết được liên kết nói. Giải pháp đúng là sử dụng lớp proxy trỏ đến lớp container cùng với cơ chế an toàn để chỉ vị trí. Trả lại vectơ begin()end()rất nguy hiểm vì (1) các kiểu đó là các vòng lặp vectơ (các lớp) ngăn không cho một người chuyển sang vùng chứa khác như a set. (2) Nếu vectơ được sửa đổi (ví dụ: đã tăng hoặc xóa một số mục), một số hoặc tất cả các trình lặp vectơ có thể đã bị vô hiệu.
rwong

Câu trả lời:


25

cho phép lặp mà không rò rỉ các phần bên trong chính xác là những gì mẫu lặp lặp hứa hẹn. Tất nhiên đó chủ yếu là lý thuyết nên đây là một ví dụ thực tế:

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

Bạn cung cấp tiêu chuẩn beginendphương thức, giống như các chuỗi trong STL và thực hiện chúng đơn giản bằng cách chuyển tiếp đến phương thức của vectơ. Điều này không rò rỉ một số chi tiết triển khai cụ thể là bạn đang trả về một trình vòng lặp vector nhưng không có máy khách lành mạnh nào nên phụ thuộc vào điều đó vì vậy nó không phải là vấn đề đáng lo ngại. Tôi đã hiển thị tất cả các tình trạng quá tải ở đây nhưng tất nhiên bạn có thể bắt đầu bằng cách chỉ cung cấp phiên bản const nếu khách hàng không thể thay đổi bất kỳ mục nhập Mọi người. Sử dụng cách đặt tên tiêu chuẩn có lợi ích: bất kỳ ai đọc mã ngay lập tức đều biết rằng nó cung cấp phép lặp 'tiêu chuẩn' và như vậy hoạt động với tất cả các thuật toán phổ biến, phạm vi dựa trên các vòng lặp, v.v.


lưu ý: mặc dù điều này chắc chắn hoạt động và được chấp nhận, đáng để lưu ý các ý kiến ​​của rwong cho câu hỏi: thêm một trình bao bọc / proxy bổ sung xung quanh các trình vòng lặp của vector ở đây sẽ làm cho khách hàng độc lập với trình lặp bên dưới thực tế
stijn

Ngoài ra, lưu ý rằng việc cung cấp một begin()end()chỉ chuyển tiếp đến vectơ begin()end()cho phép người dùng sửa đổi các phần tử trong chính vectơ, có thể sử dụng std::sort(). Tùy thuộc vào những gì bất biến mà bạn đang cố gắng bảo tồn, điều này có thể hoặc không thể chấp nhận được. Tuy nhiên, việc cung cấp begin()end(), là cần thiết để hỗ trợ các vòng lặp dựa trên phạm vi C ++ 11.
Patrick Niedzielski

Có lẽ bạn cũng nên hiển thị cùng một mã bằng cách sử dụng tự động như các kiểu trả về của các hàm lặp khi sử dụng C ++ 14.
Klaim

Làm thế nào là điều này ẩn các chi tiết thực hiện?
BЈовић

@ BЈовић bởi không để lộ vector hoàn chỉnh - ẩn không có nghĩa là việc thực hiện phải được ẩn theo nghĩa đen từ một tiêu đề và đặt trong file nguồn: nếu đó là khách hàng cá nhân không thể truy cập nó anyway
Stijn

4

Nếu lặp đi lặp lại là tất cả những gì bạn cần, thì có lẽ một trình bao bọc xung quanh std::for_eachsẽ đủ:

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

Có lẽ tốt hơn để thực thi một lặp đi lặp lại với cbegin / cend. Nhưng giải pháp đó tốt hơn nhiều so với việc cấp quyền truy cập vào vùng chứa bên dưới.
galop1n

@ galop1n Nó không thực thi một constlần lặp. Đây for_each()là một constchức năng thành viên. Do đó, các thành viên peopleđược coi là const. Do đó, begin()end()sẽ quá tải như const. Do đó, họ sẽ trở lại const_iterators để people. Do đó, f()sẽ nhận được a People const&. Viết cbegin()/ cend()ở đây sẽ không thay đổi gì, trong thực tế, mặc dù là một người dùng ám ảnh của consttôi có thể cho rằng nó vẫn đáng để làm, vì (a) tại sao không; đó chỉ là 2 ký tự, (b) Tôi thích nói những gì tôi muốn nói, ít nhất là với const, (c) nó bảo vệ chống lại việc vô tình dán ở đâu đó không const, v.v.
underscore_d

3

Bạn có thể sử dụng thành ngữ pimpl và cung cấp các phương thức để lặp lại trên container.

Trong tiêu đề:

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

Trong nguồn:

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

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

Bằng cách này, nếu khách hàng của bạn sử dụng typedef từ tiêu đề, họ sẽ không nhận thấy loại container nào bạn đang sử dụng. Và các chi tiết thực hiện được ẩn hoàn toàn.


1
Đây là ĐÚNG ... hoàn thành ẩn thực hiện và không có chi phí bổ sung.
Trừu tượng là tất cả.

2
@Astrstriseiseverything. " không có chi phí bổ sung " hoàn toàn sai. PImpl thêm phân bổ bộ nhớ động (và, sau này, miễn phí) cho mọi trường hợp và chỉ định con trỏ (ít nhất là 1) cho mọi phương thức đi qua nó. Cho dù đó là quá nhiều chi phí cho bất kỳ tình huống cụ thể nào tùy thuộc vào điểm chuẩn / hồ sơ, và trong nhiều trường hợp, điều đó có thể hoàn toàn tốt, nhưng nó hoàn toàn không đúng - và tôi nghĩ khá vô trách nhiệm - để tuyên bố rằng nó không có chi phí.
gạch dưới

@underscore_d Tôi đồng ý; không có nghĩa là vô trách nhiệm ở đó, nhưng, tôi đoán rằng tôi đã rơi vào tình huống. Không có chi phí bổ sung ... Kỹ thuật không chính xác, như bạn đã chỉ ra một cách khéo léo; xin lỗi ...
Trừu tượng là tất cả.

1

Người ta có thể cung cấp các chức năng thành viên:

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

Cho phép truy cập mà không để lộ chi tiết triển khai (như tiếp giáp) và sử dụng các chi tiết này trong một lớp lặp:

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

Lặp lại sau đó có thể được trả về bởi sổ địa chỉ như sau:

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

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

Có lẽ bạn cần phải bổ sung lớp lặp đi lặp lại với các đặc điểm, v.v. nhưng tôi nghĩ rằng điều này sẽ làm những gì bạn đã yêu cầu.


1

nếu bạn muốn thực hiện chính xác các chức năng từ std :: vector, hãy sử dụng kế thừa riêng như bên dưới và kiểm soát những gì được phơi bày.

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

Chỉnh sửa: Điều này không được đề xuất nếu bạn cũng muốn ẩn cấu trúc dữ liệu nội bộ, ví dụ: std :: vector


Kế thừa trong tình huống như vậy là tốt nhất là rất lười biếng (bạn nên sử dụng bố cục và cung cấp các phương thức chuyển tiếp, đặc biệt là vì có rất ít để chuyển tiếp ở đây), thường gây nhầm lẫn và bất tiện (nếu bạn muốn thêm các phương thức của riêng mình xung đột với các phương thức vector, thứ mà bạn không bao giờ muốn sử dụng nhưng dù sao cũng phải thừa hưởng?), và có thể gây nguy hiểm tích cực (điều gì xảy ra nếu lớp bị lười biếng thừa kế có thể bị xóa thông qua một con trỏ đến loại cơ sở đó ở đâu đó, nhưng nó [vô trách nhiệm] không bảo vệ chống lại sự phá hủy một obj dẫn xuất thông qua một con trỏ như vậy, vì vậy chỉ cần phá hủy nó là UB?)
underscore_d
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.