K-D-G
K-D-G

Reputation: 287

Accessing polymorphic members of a struct that is stored in a vector C++

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

Answers (2)

Tony Delroy
Tony Delroy

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 Bs: - 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

Tarek Dakhran
Tarek Dakhran

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

Related Questions