Reputation: 287
I have a struct A and struct B (B inherits from A). Is there a way to create std::vector that has template type A but can accept B type structs and when iterated through I can access members exclusive to struct B (obviously checking to make sure that it is of type B).
Upvotes: 1
Views: 178
Reputation: 106068
Tarek's answer's works for the common case of when it's fine to use dynamic memory allocation (which is usually better if sizeof(B)
is significantly larger than sizeof(A)
). I'm not trying to compete with that answer, but just adding some discussion points. In many (but far from all) problem domains it's considered a poor practice to dynamic_cast
, rather than add to A
a virtual void test() { }
- note that the function body does nothing - then make B
's test
an override (i.e. void test() override { ...existing body...}
). That way, the loop can just say ptr->test()
without caring about the runtime type (i.e. whether it's actually a B
object). This approach makes more sense if the "test
" operation makes some kind of logical sense for the entire heirarchy of classes, but there's just nothing worth testing in A
right now, especially if when you add a C
type derived from A
either directly or via B
it will also want a test
function called from the loop: you don't really want to have to go to every such loop and add an extra dynamic_cast<>
test.
Just for the fun of an alternative that happens to be closer to your request for a vector
that can "can accept B
type structs" (albeit no longer a vector<A>
), you can get much the same results using std::variant
and have either an A
or a B
stored directly inside the vector
-managed contiguous memory, which works best if there's little size difference or memory usage doesn't matter, but the objects are small enough that CPU cache locality is useful for performance.
#include <vector>
#include <iostream>
#include <variant>
struct A {
virtual void f() const { /* do nothing */ }
};
struct B : A {
int i_;
B(int i) : i_{i} { }
void f() const override { std::cout << "B\n"; }
void g() const { std::cout << "B::g() " << i_ << '\n'; }
};
int main()
{
std::vector<std::variant<A, B>> v{ A{}, A{}, B{2}, A{}, B{7}, B{-4}, A{} };
for (const auto& x : v)
if (const auto* p = std::get_if<B>(&x))
p->g();
}
Separately, the reason you can't simply use a vector<A>
and overwrite some of the elements with B
objects is that lying to the compiler that way creates undefined behaviour. For an example of why this might be a forbidden by the language, consider that for normal code generation the compiler should be able to rely on compile-time knowledge that the vector
only stores A
-type objects, and e.g. hardcode a nullptr
return from any dynamic_cast<B*>
(or throw from dynamic_cast<B&>
). You might think it should be forbidden for the compiler not to do run-time checks when you're using run-time polymorphism, but the reality is the other way around - compiler optimisers try very hard to identify situations where the run-time type is known and virtual dispatch can be avoided, as that avoids a bit of indirection and an out-of-line function call, and may allow dead-code elimination (i.e. if test()
does nothing, don't generate any code for the call thereto).
Other practical problems with selectively overwriting A
objects in a vector
with B
s:
- the wrong element destructor will be called when the vector
's destructed
- should anyone ever add data members of basses to B
such that sizeof(B) > sizeof(A)
, placement new
-ing a B
object into the vector
will overwrite memory for the following object (or off the end of the vector
).
There is something I'm told is called the Copeland Virtual Constructor Idiom where an object's type is changed similar to operator new(&a) B{}
(though in that case, it may done from the constructor operator new(this) B{}
), but you'd have to be a language and/or implementation expert to know if/when it's safe to use.
Upvotes: 1
Reputation: 2161
You can use store your objects as pointers and use dynamic_cast
to check downcast.
#include <iostream>
#include <memory>
#include <vector>
struct A {
virtual ~A() = default;
};
struct B : public A {
void test() { std::cout << "I'm B(" << this << ")" << std::endl; }
};
int main() {
std::vector<std::unique_ptr<A>> elements;
elements.push_back(std::make_unique<A>());
elements.push_back(std::make_unique<B>());
for (const auto &e : elements) {
if (auto ptr = dynamic_cast<B *>(e.get())) {
ptr->test();
}
}
return 0;
}
Upvotes: 2