Fabio C.
Fabio C.

Reputation: 347

Modern iterating c++ collection with filter

Let say I have this class design

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

and a container of A like this

std::list<A *> elements;

Now what I want to achieve is iterate through all B objects in my container, or, in another time, iterate through all C objects.

The classic way would be

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

One idea that comes to my mind is creating an iterator class derived from standard that filters but it would be difficult. No limits on the c++ language level (c++20 may be ok as well but it would be great to see C++11 replies). Plain c++ and stl please (I know boost has some foreach if construct but).

Upvotes: 3

Views: 1981

Answers (5)

Carsten
Carsten

Reputation: 11606

I think in C++11 the way you described is as close as it gets, but I may be wrong on this. C++17 greatly extended the algorithms library, so you could use std::for_each.

To demonstrate this, let's give the classes a little bit of functionality and create a vector (or list) of instances:

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

Now using for_each, you could re-write your loop:

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

Unfortunately, there is no builtin filter for any of the algorithms. You could, however, implement something like 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);
    });
}

And use it like this:

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

Or for your specific case, you could specialize the implementation even more:

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

and use it like so:

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

All three implementations print the same output:

Class B
Class B
Class B

Upvotes: 1

Vasilij
Vasilij

Reputation: 1941

One of the possible solutions without dynamic_cast. But care should be taken to state the correct type in derived class constructors.

And I would recommend to use std::unique_ptr if the list actually stores the class objects.

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

Upvotes: 1

Cleiton Santoia Silva
Cleiton Santoia Silva

Reputation: 483

I can add 2 cents: normally this smells like a design flaw ( sure there are exceptions ), this problem of "heteroganeous container", does not have a "good" solution so far. Something I have seen in th wilds is that on top of std:vector<A*> va with all elements, you may maintain another vector only with "B*" objects, std::vector<B*> vb, when it´s time to iterate go for vb when it´s time to delete go for va

Upvotes: 1

Picaud Vincent
Picaud Vincent

Reputation: 10982

A possible c++20 implementation using range

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

Prints:

B

B

Demo: https://godbolt.org/z/6oP8hj

Note: if performance matter you can avoid using dynamic_cast two times by

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

Upvotes: 2

463035818_is_not_an_ai
463035818_is_not_an_ai

Reputation: 122133

You do not need to cast if you got the design right:

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

Then your loop is simply:

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

Upvotes: 0

Related Questions