基于Pimpl惯用法实现C++类STL兼容可迭代性(不暴露实现)
Great question—this is a common scenario when using the Pimpl idiom while wanting to maintain STL compatibility without leaking internal details. Here's a robust solution that addresses both your requirements:
Core Approach
We'll create a custom STL-compliant iterator type in the StreamIterator header, exposing only the minimal required interface. All iteration logic, filtering, and internal state will be hidden in the .cpp file (either via the existing StreamIterator::impl or iterator-specific private state). This keeps your headers clean while supporting range-for loops and STL algorithms.
Step 1: Define the Custom Iterator in the Header
First, update your StreamIterator header to declare the custom iterator, with all STL-required type definitions and minimal public operators. We'll keep iterator internal state private and only let StreamIterator create iterator instances.
#include <iterator> #include <memory> // Forward declarations (adjust as needed for your types) class Object { public: using Ptr = std::shared_ptr<Object>; }; class Stream; class StreamFilter; class StreamIterator { public: // Custom iterator type (compliant with STL input iterator requirements) class iterator { public: // STL iterator mandatory typedefs using iterator_category = std::input_iterator_tag; using value_type = Object::Ptr; using difference_type = std::ptrdiff_t; using pointer = value_type*; using reference = value_type&; // Default/copy/move semantics iterator() = default; iterator(const iterator&) = default; iterator& operator=(const iterator&) = default; iterator(iterator&&) = default; iterator& operator=(iterator&&) = default; // Required iteration operators value_type operator*() const; iterator& operator++(); // Pre-increment iterator operator++(int); // Post-increment // Equality checks bool operator==(const iterator& other) const; bool operator!=(const iterator& other) const { return !(*this == other); } private: // Only StreamIterator can create valid iterators friend class StreamIterator; iterator(StreamIterator::impl* parent_impl, bool is_end = false); // Iterator internal state (hidden from header users) StreamIterator::impl* parent_impl_ = nullptr; bool is_end_ = true; Object::Ptr current_item_; }; // Reuse iterator for const_iterator if your elements are read-only using const_iterator = iterator; // Constructor/destructor (destructor must be defined in .cpp for Pimpl) StreamIterator(Stream* streamToFilter); ~StreamIterator(); // STL-compatible begin/end methods iterator begin(); iterator end(); const_iterator cbegin() const; const_iterator cend() const; // Filter management (use smart pointers instead of raw pointers) void addFilter(std::unique_ptr<StreamFilter> filter); void removeFilter(const StreamFilter* filter); private: class impl; std::unique_ptr<impl> pimpl_; };
Step 2: Implement the Iterator and Pimpl in the .cpp File
All the heavy lifting (filter logic, iteration state management) happens here. We'll extend the StreamIterator::impl to handle filter storage and element traversal, then implement the iterator's operators to use this logic.
#include "StreamIterator.h" #include "Stream.h" #include "StreamFilter.h" #include <algorithm> // Define the Pimpl for StreamIterator class StreamIterator::impl { public: impl(Stream* stream) : stream_(stream) {} // Filter storage and traversal state std::vector<std::unique_ptr<StreamFilter>> filters_; Stream* stream_; size_t current_pos_ = 0; bool is_at_end_ = false; // Get the next item that passes all filters Object::Ptr get_next() { while (!is_at_end_) { // Fetch the next item from the stream (adjust based on your Stream API) auto item = stream_->get_item(current_pos_); if (!item) { is_at_end_ = true; return nullptr; } current_pos_++; // Check if the item passes all filters bool passes = true; for (const auto& filter : filters_) { if (!filter->matches(item)) { // Assume StreamFilter has a `matches` method passes = false; break; } } if (passes) { return item; } } return nullptr; } // Reset traversal state for new iterations void reset() { current_pos_ = 0; is_at_end_ = false; } }; // StreamIterator constructor/destructor StreamIterator::StreamIterator(Stream* streamToFilter) : pimpl_(std::make_unique<impl>(streamToFilter)) {} StreamIterator::~StreamIterator() = default; // Iterator constructor (only called by StreamIterator) StreamIterator::iterator::iterator(StreamIterator::impl* parent_impl, bool is_end) : parent_impl_(parent_impl), is_end_(is_end) { if (!is_end_) { current_item_ = parent_impl_->get_next(); is_end_ = !current_item_; } } // Iterator operator implementations Object::Ptr StreamIterator::iterator::operator*() const { return current_item_; } StreamIterator::iterator& StreamIterator::iterator::operator++() { if (!is_end_) { current_item_ = parent_impl_->get_next(); is_end_ = !current_item_; } return *this; } StreamIterator::iterator StreamIterator::iterator::operator++(int) { iterator temp = *this; ++(*this); return temp; } bool StreamIterator::iterator::operator==(const iterator& other) const { // Two end iterators are equal if (is_end_ && other.is_end_) return true; // Non-end iterators are equal if they share the same parent and current item return (!is_end_ && !other.is_end_) && (parent_impl_ == other.parent_impl_) && (current_item_ == other.current_item_); } // StreamIterator begin/end methods StreamIterator::iterator StreamIterator::begin() { pimpl_->reset(); return iterator(pimpl_.get()); } StreamIterator::iterator StreamIterator::end() { return iterator(pimpl_.get(), true); } StreamIterator::const_iterator StreamIterator::cbegin() const { // Adjust if you need true const-correctness (e.g., const Stream access) auto non_const_this = const_cast<StreamIterator*>(this); non_const_this->pimpl_->reset(); return const_iterator(non_const_this->pimpl_.get()); } StreamIterator::const_iterator StreamIterator::cend() const { return const_iterator(const_cast<StreamIterator*>(this)->pimpl_.get(), true); } // Filter management methods void StreamIterator::addFilter(std::unique_ptr<StreamFilter> filter) { pimpl_->filters_.push_back(std::move(filter)); } void StreamIterator::removeFilter(const StreamFilter* filter) { auto it = std::remove_if(pimpl_->filters_.begin(), pimpl_->filters_.end(), [filter](const std::unique_ptr<StreamFilter>& f) { return f.get() == filter; }); pimpl_->filters_.erase(it, pimpl_->filters_.end()); }
Step 3: Usage Example
This implementation works seamlessly with range-for loops and STL algorithms:
// Example FilterByOffset implementation class FilterByOffset : public StreamFilter { public: FilterByOffset(size_t min_offset) : min_offset_(min_offset) {} bool matches(Object::Ptr item) const override { return item->get_offset() >= min_offset_; // Assume Object has get_offset() } private: size_t min_offset_; }; int main() { Stream my_stream; StreamIterator iter(&my_stream); iter.addFilter(std::make_unique<FilterByOffset>(10)); // Range-for loop for (auto item : iter) { // Process filtered items } // STL algorithm usage std::for_each(iter.begin(), iter.end(), [](Object::Ptr item) { // Process item }); return 0; }
Key Advantages
- No internal exposure: Headers never reveal your stream's underlying storage or filter implementation details.
- STL compliance: The iterator meets input iterator requirements, so it works with all STL algorithms and range-for loops.
- Filter-aware iteration: The iterator automatically skips items that don't pass your filters, so users don't need to handle filtering manually.
- Smart pointer safety: We use
std::unique_ptrfor filters to avoid memory leaks and raw pointer risks.
Notes
- Iterator category: We used
std::input_iterator_tagsince streams are typically one-way. If your stream supports bidirectional traversal, upgrade tostd::forward_iterator_tagand addoperator--. - Const correctness: If you need a true
const_iteratorthat can't modify elements, create a separate class withconst Object::Ptras the value type. - Iterator validity: Modifying filters during iteration will lead to undefined behavior—document that users should set filters before calling
begin().
内容的提问来源于stack exchange,提问作者MaxC2

