I want to make an abstract class, A that will be subclassed by Class B and Class C such that they will all use the same methods in the defined abstract class (B and C are A-able classes).
I have another class, Z, that will contain an array of A-able classes. I would like for it to have a function that allows it to swap between B and C in that array (ie. calling initializer/member function with an argument).
The below example, while not being exactly like what I'm describing above (not using abstract classes), showcases the same issue I'm running into: I'm unable to set the array to the correct subclass, since it's complaining that it was initialized as the parent class.
However, this should be possible to do right? What am I missing here?
#include <iostream>
#include <array>
class BaseItem {
protected:
std::string name;
BaseItem(const std::string & name) : name(name) {};
virtual void printName();
virtual ~BaseItem() = default;
};
class Item1: public BaseItem {
public:
using BaseItem::name;
Item1() : BaseItem("Book1") {}
void printName() {
std::cout << "1" << name;
}
};
class Item2: public BaseItem {
public:
using BaseItem::name;
Item2() : BaseItem("Book2") {}
void printName() {
std::cout << "2" << name;
}
};
class Library {
public:
std::array<BaseItem, 2> books;
void setToItem2() {
for (size_t i = 0; i < books.size(); i ) {
books[i] = new Item2();
}
}
void setToItem1() {
for (size_t i = 0; i < books.size(); i ) {
books[i] = new Item1();
}
}
void printBooks() {
for (auto& entry: books) {
entry->printName();
}
}
};
int main() {
Library a;
a.setToItem1();
a.printBooks();
a.setToItem2();
a.printBooks();
return 0;
}
Edit: Cleaned up a bit, also adding error message below:
prog.cpp: In member function ‘void Library::setToItem2()’:
prog.cpp:36:31: error: no match for ‘operator=’ (operand types are ‘std::array<BaseItem, 2>::value_type’ {aka ‘BaseItem’} and ‘Item2*’)
Edit2: Made the example code more representative of what I want to implement, utilizing code help from some of the existing answers.
Current potential solutions:
- Evict books and pass in the correct subclass. This is currently what I'm going with. Just don't know if there is anything that can make this look cleaner (ie. all the casting looks a bit messy).
- Make books a variant. The code looks cleaner here, but if I'm to extend to Item3, Item4, etc. I'll have to increase the variant to include all those subtypes, which IMHO defeats part of the purpose of making this "interface" (of course, we still get to inherit some shared things, but I'd like to not have to keep adding new classes into variant).
For now, I'm going to just do 1. But please let me know if there is something better.
CodePudding user response:
Like other comments, if you store a vector of superclass by value, say vector<A>
, as the vector allocates the memory, in addition to other information that vector stores, it will allocate sizeof(A)*NumOfElement(vector<A>)
for storage. As subclasses, say B
need more space than A, object slicing will occur. My suggestion is, instead of storing the class as value, store those as reference. ex)vector<shared_ptr<A>>
. As the size of the pointer is same, this will allow to store A's subclasses. Oh, do not forget to define its virtual destructor!
Suggested code:
#include <iostream>
#include <vector>
#include <memory>
class Item {
public:
Item() : name("Book1") {}
std::string name;
virtual void f1() {/* Your Implementation here or make it pure virtual */};
virtual ~Item() = 0;
};
class Item2 : public Item {
public:
Item2() { name = "Book2"; }
//std::string name; //Hides base class name
void f1() override {/* Your Implementation here */};
~Item2() = default;
};
class Library {
public:
std::vector<std::shared_ptr<Item>> books;
void setToItem2() {
books.emplace_back(std::dynamic_pointer_cast<Item>(std::shared_ptr<Item2>(new Item2()))); //If you wish, use loop here
books.emplace_back(std::dynamic_pointer_cast<Item>(std::shared_ptr<Item2>(new Item2())));
}
void printBooks() {
for (auto& entry : books) {
std::cout << entry->name;
}
}
};
int main() {
Library a;
a.printBooks();
return 0;
}
CodePudding user response:
The blessed way to store polymorphic instances in a container is to use std::unique_ptr
. The container is still the sole owner of the object, but that pattern does not suffer the object slicing problem.
Furthermore your class hierarchy is weird: an Item2
instance will contain two versions of name. One (not directly accessible) in its Item
base class and one directly accessible. It should at least be:
class Item2 : public Item {
public:
using Item::name;
Item2() {
name = "Book2";
}
};
But at construction time, name
will first receive "Book1"
at the base class initialization time, and then "Book2"
. So the normal way would be to build a base class like:
class BaseItem {
protected:
std::string name;
BaseItem(const std::string & name) : name(name) {};
virtual ~BaseItem() = default;
};
class Item: public BaseItem {
public:
using BaseItem::name;
Item() : BaseItem("Book1") {}
};
You can now build your Library
class:
class Library {
public:
std::array<std::unique_ptr<BaseItem>, 2> books;
void printBooks() {
for (auto& entry : books) {
std::cout << entry->name;
}
}
};
Alternatively if you want to stick to a swapping pattern, you should use a variant:
class Library {
public:
std::variant<std::array<Item, 2>, std::array<Item2, 2> > books = std::array<Item, 2>();
void setToItem2() {
books = std::array<Item2, 2>();
}
void printBooks() {
auto *b = std::get_if< std::array<Item, 2> >(&books);
if (nullptr != b) {
for (auto& entry : *b) {
std::cout << entry.name << "\n";
}
}
else {
auto* b2 = std::get_if< std::array<Item2, 2> >(&books);
for (auto& entry : *b2) {
std::cout << entry.name << "\n";
}
}
}
};
int main() {
Library a;
a.printBooks();
a.setToItem2();
a.printBooks();
return 0;
}