PAYATHON 2026

C++ 中如何通过抽象类提供可用于 range-based for 的范围?

数据小陈

可以。抽象类可以提供虚函数 begin(),返回一个能够参与循环操作的具体类型。常见做法是用具体的迭代器包装类持有抽象迭代状态,将读取、递增和结束判断转发给虚函数。

下面的 C++17 实现使用不同类型的迭代器和结束哨兵完成遍历。

原代码为什么不能直接用于循环?

原代码有几处问题:

  • getTestRangeInitializer() 返回的是指针。range-based for 不会自动解引用这个指针,再调用对象的 begin() 和 end()。
  • begin() 返回 TestRange*,end() 返回 int,两者没有用于判断循环结束的 != 操作。
  • 对 TestRange* 执行 ++ 是指针运算,不会修改对象的 index。
  • 对 TestRange* 执行一次 * 得到的是 TestRange&,不会继续调用 TestRange::operator*(),因此不能直接初始化循环变量 int i。

TestRange 中定义的比较运算符接收对象引用,也不会自动用于两个指针的比较。

range-based for 需要哪些操作?

对这里的类范围,可以把循环理解为下面的形式,省略了编译器内部变量名等细节:

auto&& range = *initializer;
auto it = range.begin();
auto last = range.end();

for (; it != last; ++it) {
    int i = *it;
    std::cout << i << ' ';
}

begin() 返回的类型需要支持 *it、++it,以及与 end() 返回值的比较。

从 C++17 开始,begin() 和 end() 可以返回不同类型,只要相应的比较表达式成立。因此可以用一个简单的 Sentinel 表示结束。

一个完整的 C++17 实现

保留 A、TestRange 和 TestRangeInitializer 的基本分工,增加具体的 Iterator 包装类:

#include <array>
#include <cstddef>
#include <iostream>
#include <memory>
#include <utility>

class A {
public:
    // 不同派生类提供各自的遍历状态和行为。
    class TestRange {
    public:
        virtual ~TestRange() = default;
        virtual int operator*() const = 0;
        virtual void advance() = 0;
        virtual bool done() const = 0;
    };

    struct Sentinel {};

    // range-based for 实际操作的具体类型。
    class Iterator {
    public:
        explicit Iterator(std::unique_ptr<TestRange> cursor)
            : cursor_(std::move(cursor)) {}

        int operator*() const {
            return **cursor_;
        }

        Iterator& operator++() {
            cursor_->advance();
            return *this;
        }

        bool operator!=(Sentinel) const {
            return cursor_ && !cursor_->done();
        }

    private:
        std::unique_ptr<TestRange> cursor_;
    };

    class TestRangeInitializer {
    public:
        virtual ~TestRangeInitializer() = default;
        virtual Iterator begin() const = 0;

        Sentinel end() const {
            return {};
        }
    };

    virtual ~A() = default;
    virtual std::unique_ptr<TestRangeInitializer>
    getTestRangeInitializer() const = 0;
};

class B : public A {
    class TestRangeB : public TestRange {
    public:
        int operator*() const override {
            return data_[index_];
        }

        void advance() override {
            ++index_;
        }

        bool done() const override {
            return index_ >= data_.size();
        }

    private:
        std::array<int, 3> data_ = {1, 3, 5};
        std::size_t index_ = 0;
    };

    class TestRangeInitializerB : public TestRangeInitializer {
    public:
        Iterator begin() const override {
            return Iterator(std::make_unique<TestRangeB>());
        }
    };

public:
    std::unique_ptr<TestRangeInitializer>
    getTestRangeInitializer() const override {
        return std::make_unique<TestRangeInitializerB>();
    }
};

int main() {
    std::unique_ptr<A> obj = std::make_unique<B>();
    auto initializer = obj->getTestRangeInitializer();

    for (int i : *initializer) {
        std::cout << i << ' ';
    }
    std::cout << '\n';
}

按照这段代码,循环依次输出 1 3 5。

Iterator 的静态类型始终相同,但它持有的 TestRange 可以是 TestRangeB,也可以是以后实现的 TestRangeC。解引用、递增和判断结束时,都会调用对应派生类的实现。

实现时需要注意什么?

结束状态应与元素值分开。原代码用 -1 表示遍历结束,会与合法的 -1 元素产生歧义。示例中的 done() 根据位置判断是否结束,不依赖元素内容。

多态对象需要明确的所有权。示例用 std::unique_ptr 管理对象,并为通过基类指针销毁的类型提供虚析构函数。initializer 保存为局部变量,确保范围对象在循环期间存活。

这个包装类只提供当前循环所需的操作。它可以用于这里的 range-based for,但没有提供复制、后置递增、迭代器之间的比较等通用接口,不能据此认为它满足标准库算法的迭代器要求。接入算法时,需要根据目标算法补齐相应能力。

使用 C++11 或 C++14 时,应让 begin() 和 end() 返回相同的迭代器类型,并在该类型中实现结束状态及比较。上面的独立 Sentinel 写法要求 C++17 或更新标准。

备注:内容仅供参考。