Reputation: 347
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
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
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
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
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
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