【问题标题】:Modern iterating c++ collection with filter带有过滤器的现代迭代 C++ 集合
【发布时间】:2020-07-14 06:26:42
【问题描述】:

假设我有这个类设计

class A {};
class B : public A {};
class C : public A {};

和这样的 A 容器

std::list<A *> elements;

现在我想要实现的是遍历容器中的所有 B 对象,或者,换个时间,遍历所有 C 对象。

经典的方法是

for (auto it = elements.begin(); it != elements.end(); ++it) {
  B * b = dynamic_cast<B *>(*it);
  if (b) {
    // do stuff
  }
}

我想到的一个想法是创建一个从过滤标准派生的迭代器类,但这很困难。 c++ 语言级别没有限制(c++20 可能也可以,但很高兴看到 C++11 的回复)。 请纯 c++ 和 stl(我知道 boost 有一些 foreach if 构造但是)。

【问题讨论】:

  • 您能告诉我们您想在循环中制作什么stuff吗?因为您可能只声明一个接口就足够了
  • 就我而言,主要是列举,但我想谈一谈。我的具体情况如下:基础是属性,派生是 DisplayProperty、BehaviourProperty、EventsProperty。所有这些都在一个属性集合中,但是在一个窗口中我只想显示所有应用的 DisplayProperty。如果有某种视图在内容上仅列出该类型,那就太好了

标签: c++ stl iterator


【解决方案1】:

使用范围的可能的 c++20 实现

#include <iostream>
#include <list>
#include <ranges>

struct A {
  virtual ~A() = default;
};
struct B : public A {
  void foo() const { std::cout << "B\n"; }
};
struct C : public A {};

int main() {
  std::list<A *> demo{new A{}, new B{}, new C{}, new B{}};
  auto is_B = [](const A *p) { return dynamic_cast<const B *>(p) != nullptr; };
  auto get_B_const = [](const A *p) { return dynamic_cast<const B *>(p); };

  for (auto p_B :
       demo | std::views::filter(is_B) | std::views::transform(get_B_const)) {
    p_B->foo();
  }
  // demo destruction with delete not shown
}

打印:

B

B

演示:https://godbolt.org/z/6oP8hj

注意:如果性能很重要,您可以避免使用 dynamic_cast 两次

 auto get_B_const = [](const A *p) { 
    assert(dynamic_cast<const B *>(p));
    return static_cast<const B *>(p);
 };

【讨论】:

  • 这非常简洁,我喜欢这段代码的简洁和自动解释
  • 我正在尝试在范围上记录自己,但我不明白迭代器(在您的示例中为 p_B)是否可以用于前后移动,或者这种技术是否可以仅枚举每个包含的元素。
【解决方案2】:

我可以加 2 美分:通常这闻起来像一个设计缺陷(当然也有例外),这个“异构容器”的问题,到目前为止还没有一个“好的”解决方案。我在 th wilds 中看到的是,在所有元素的 std:vector&lt;A*&gt; va 之上,您可以只使用“B*”对象维护另一个向量 std::vector&lt;B*&gt; vb,当它需要迭代时,请使用 vb是时候删除va

【讨论】:

  • 保持不同的容器肯定是一种可能的解决方案,到目前为止,带有视图的 c++20 范围似乎是一个很好的方法,可以让您在关系数据库及其视图中找到一种模式。请注意我是一个实际的例子,但它也可以应用于同质容器。例如,让我们在具有某些特征的每个元素上创建一个相同类的容器和一个迭代器
【解决方案3】:

没有 dynamic_cast 的可能解决方案之一。但应注意在派生类构造函数中声明正确的类型。

如果列表实际存储类对象,我建议使用 std::unique_ptr。

class Base
{
public:
  enum class Type
  {
    A,
    B,
    C
  };
  Base() = delete;
  virtual ~Base() = default;    
  Type type() const { return _type; }

protected:
  Base(Type type) : _type{type} {}  

private:
  Type _type;
};

class A : public Base
{
public:
  A() : Base{Base::Type::A} {}
};

class B : public Base
{
public:
  B() : Base{Base::Type::B} {}
};
class C : public Base
{
public:
  C() : Base{Base::Type::C} {}
};
            
    void function() 
    {
       std::list<std::unique_ptr<Base>> list;
       list.emplace_back(std::make_unique<A>());
       list.emplace_back(std::make_unique<B>());
       list.emplace_back(std::make_unique<C>());
                   
       // use non-const iterators if you intend to modify the object
       std::for_each(std::cbegin(list), std::cend(list),
                     [](const auto &item)
                     {
                       switch (item->type()) 
                       {
                         case Base::Type::B: 
                         {
                           assert(dynamic_cast<B*>(item.get()));
                           const auto &b = static_cast<B*>(item.get());
                           // do staff with b
                           break;
                         }
          
                         default:
                           return;
                         }                          
                   });
    }

【讨论】:

【解决方案4】:

我认为在 C++11 中,您所描述的方式已经非常接近,但我可能错了。 C++17大大扩展了algorithms library,所以你可以使用std::for_each

为了演示这一点,让我们给类一些功能并创建一个实例向量(或列表):

class A {
public:
    virtual std::string name() const = 0;
};
class B : public A {
public:
    virtual std::string name() const override {
        return "Class B";
    }
};
class C : public A {
public:
    virtual std::string name() const override {
        return "Class C";
    }
};

int main()
{
    std::vector<A*> vec { new B(), new B(), new C(), new C(), new B() };
}

现在使用for_each,您可以重新编写循环:

std::for_each(std::begin(vec), std::end(vec), [](const A* val) {
    auto B* b = dynamic_cast<B*>(val);

    if (b)
        std::cout << b->name() << std::endl;
});

不幸的是,任何算法都没有内置过滤器。但是,您可以实现类似for_each_if:

template<typename Iterator, typename Predicate, typename Operation> void 
for_each_if(Iterator begin, Iterator end, Predicate pred, Operation op) {
    std::for_each(begin, end, [&](const auto p) {
        if (pred(p))
            op(p);
    });
}

并像这样使用它:

for_each_if(std::begin(vec), std::end(vec), 
    [](A* val) { return dynamic_cast<B*>(val) != nullptr; },
    [](const A* val) {
        std::cout << val->name() << std::endl;
    }
);

或者对于您的具体情况,您可以更加专门化实现:

template<typename T, typename Iterator, typename Operation> void 
dynamic_for_each(Iterator begin, Iterator end, Operation op) {
    std::for_each(begin, end, [&](auto p) {
        auto tp = dynamic_cast<T>(p);
        
        if (tp)
            op(tp);
    });
}

并像这样使用它:

dynamic_for_each<B*>(std::begin(vec), std::end(vec), [](const B* val) {
    std::cout << val->name() << std::endl;
});

所有三个实现都打印相同的输出:

B类 B类 B类

【讨论】:

    【解决方案5】:

    如果设计正确,则无需投射:

    struct A { 
        virtual void doSomethingWithB() = 0;
        virtual ~A() = default;
    };
    struct B : A {
        void doSomethingWithB() override {
            // do somehting
        }
    };
    struct C : A {
        void doSomethingWithB() override {
           // do nothing !
        }
    };
    

    那么你的循环就是:

    for (auto elem : elements) {
        elem->doSomethingWithB();
    }
    

    【讨论】:

    • 你是对的,但是这个设计也有一个问题,就是如果要添加一个新的类型D,那么我也必须更改A来添加doSomethingWithD
    • @FabioC 实际的问题似乎是当它们实际上没有共同点时,您试图将不同的类压缩到一个基类中;)
    • 事实上正是如此。更准确地说,它是一个伪 html 解析器,在列表中我保持读取每个实体。有时它们是节点,有时它们是属性,有时它们是 cmets。我想按读取顺序保留每个元素的树,但有时我只需要迭代节点,有时只需要元素,有时只需要评论。也许从一个元素我想看到以前的评论
    猜你喜欢
    • 2012-04-19
    • 2020-09-28
    • 1970-01-01
    • 2019-08-22
    • 1970-01-01
    • 2012-07-07
    • 1970-01-01
    • 2010-12-26
    • 2015-04-18
    相关资源
    最近更新 更多