1

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:

  1. 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).
  2. 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.

dcheng
  • 1,827
  • 1
  • 11
  • 20
  • Why do `Item2` have its own `name` field? – Some programmer dude Feb 10 '22 at 08:18
  • 1
    As you already use `std::` for most things, also do it for the rest and remove `using namespace std;`. Also don't use empty constructors. Either `=default` or better: don't have it. – JHBonarius Feb 10 '22 at 08:19
  • I think you need an array of `Item&` elements, not an array of `Item` elements. –  Feb 10 '22 at 08:19
  • 1
    Also note that when you add an `Item2` to your `books` array, you will have [*object slicing*](https://stackoverflow.com/questions/274626/what-is-object-slicing). That means you don't have anything `Item2` specific left in the array, it only stores `Item` objects. And you can't change the type of a variable dynamically at run-time. – Some programmer dude Feb 10 '22 at 08:19
  • For object slicing, if Item2 is identical to Item, except just overriding the functions to do Item2 things, then would this still be ok? or would calling the functions still be Item functions and not Item2 ones? I cleaned up the code a bit. Removing name from Item2 seems to cause a compiler issue. – dcheng Feb 10 '22 at 08:28
  • 1
    @dcheng This: `std::array` is not the same type as `std::array`, It is as different as a car and an elephant. Just because they look alike doesn't mean they are the same or can be assigned to each other. The second thing is that currently, your `Item` class lacks a virtual destructor, thus is unsafe to be used for polymorphism purposes. – PaulMcKenzie Feb 10 '22 at 08:32

2 Answers2

2

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;
}
K.R.Park
  • 1,015
  • 1
  • 4
  • 18
  • Just to make sure, because I'm quite a noob: If Item has a function, f1, that Item2 overrides. And now something in Library, post the call of setToItem2, calls f1, will it use Item's f1 or Item2's f1? I'd want it to use Item2's. – dcheng Feb 10 '22 at 09:01
  • 1
    then define the `Item::f1` as a virtual function, and override `Item2::f1`! – K.R.Park Feb 10 '22 at 09:03
  • Yep, just confirmed. This seems like the best solution so far. Thanks! So in general, the only way I can do this "swap" is to basically swap in the new class. Is there a way to just simply tell f1 to operate on Item as if its Item2 when I want it to, or operate on it as if it's Item3, etc via arguments/flags? – dcheng Feb 10 '22 at 09:11
  • 1
    @dcheng currently, virtual function and dynamic casting is best way you can consider. Although there is CRTP pattern for doing similar purpose, it is somewhat differ, and its syntax is more complicated. – K.R.Park Feb 10 '22 at 09:19
  • Oh ok thanks. I'll take a look to see if CRTP is what I'd use. – dcheng Feb 10 '22 at 09:26
1

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;
}
Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • So for swapping, to use say item2 instead of item, we'd have to iterate through books and reassign each entry (of which there are 2) just like in the above answer right? (Assuming BaseItem has more virtual functions that Item and Item2 implements) Or is there a better way of doing the swap? – dcheng Feb 10 '22 at 09:20
  • Thanks for the edit. So let's bring in your BaseItem (abstract class) and have two subclasses Item1 and Item2. Is variant the only way to have "books" be able to hold Item1 and Item2, or is there a way for that collection to hold any subclass of BaseItem and I can just setToItem2/setToItem1 just like you did here? The idea is that in the future, I might have Item3, and I'm not sure if the right strategy is to just keep adding Item3, Item4, etc... to this variant, when the only difference between these items are what their implemented functions do. – dcheng Feb 10 '22 at 21:58
  • 1
    @dcheng Oh, In that case, Design pattern named Strategy is clearly you would wish to look. – K.R.Park Feb 11 '22 at 01:01