zebediah49
zebediah49

Reputation: 7611

C++ container full of abstract objects

My background is primarily C and Java (with a smattering of other things), and have started working on a project in C++. My exact task is generating a set of objects from a configuration file (that is, read the file, determine what sort of object to create, add it to a list, repeat). The problematic part is equivalent to the following:

(I'm using C char* syntax because I can easily write it for the example here -- presume I don't actually know how many I need as well, which is why I would like to use vector)

class General {
    public:
        virtual int f()=0;
};
class SpecificA : public General {
    public:
        virtual int f();
};
class SpecificB : public General {
    public:
        virtual int f();
};
std::vector<General> build(char** things, int number) {
    std::vector<General> result;
    for(int i=0;i<number;i++) {
        if(strcmp(things[i],"A")) {
            result.push_back(SpecificA());
        } else if(strcmp(things[i],"B")) {
            result.push_back(SpecificB());
        }
    }
    return result;
}

This won't work, because I can't have a vector of General, because you can never actually have an instance of General. From various places I have read,

It's beginning to feel a little like C++ wants me to use the memory practices of Java using the tools of C, and it's really not working. At this point I'm getting quite tempted to throw any kind of best practices involving automatic memory allocation out the window and litter my code with *'s, but I'd like to hear if there's a "right" way of doing this first.

Upvotes: 2

Views: 232

Answers (4)

Nikita Kakuev
Nikita Kakuev

Reputation: 1136

The main problem in your code is that you're trying to work with polymorphic objects directly, rather than by pointers or references. And C++ doesn't work this way.

Objects in C++ have (among other things) a size. This size is fixed and doesn't change. All C++ machinery is built around this idea. An object of class A it occupies sifeof(A) bytes. An array of n objects of class A occupies n * sizeof(A) bytes since it contains n continuously allocated objects of type A. Having this fixed size makes pointer arithmetics and constant-time member access operator possible.

Now, objects of different classes within a hierarchy can have different sizes. Here's an example:

struct Base {
   virtual void f() { std::cout << "Base\n"; }
};

struct Derived: Base {
    void f() override { std::cout << "Derived\n"; }
    int m_value = 0;
};

It's pretty obvious that sizeof(Base) != sizeof(Derived) since the latter has the m_value member. So in order to store any object of this hierarchy (that is either Base or Derived) you now need at least sizeof(Derived) bytes. This is:

  1. Wasteful
  2. Not easily achievable since your class hierarchy can be scattered across different translation units (you can have another class that derives from Base and has additional class members)

Both of this problems can be solved with indirection. Instead of storing the objects themselves you can store something that is fixed in size, but can point to objects of different sizes. A pointer, for example. A smart one preferably.

The difference between Java and C++ here is that Java automatically employs indirection for you, while C++ wants you to do this yourself. And as long as you use smart pointers you don't violate any of "best practices involving automatic memory allocation".

TL;DR The best practice in your case is to use std::vector<std::unique_ptr<General>>.

Upvotes: 2

Puppy
Puppy

Reputation: 146930

Large structs (such as classes) should be passed by reference rather than copied around and returned from functions

That's bullshit. On the contrary, you should always take and return by value unless there's a good reason not to. For the record, this applies to string too - I know you said you used char* for convenience of writing this question, but for future readers of this question, don't do that in your actual code. Use std::string, always.

A simple smart pointer (as in Ilya Kobelevskiy's answer) is more than sufficient to solve this problem.

There are other solutions too. For example, you could instead take a function and iterate over the objects, which doesn't require this.

template<typename T> void build(std::vector<std::string> things, T func) {
    for(auto str : things) {
        if(str == "A") {
            func(SpecificA());
        } else if(str == "B")) {
            func(SpecificB());
        }
    }
}

Now you may use it as a lambda.

int main() {
    auto things = /* insert things here */;
    build(things, [](const General& g) {
        // Don't miss the const, it's kinda important.
        g.f();
    });
}

Now you don't even have the problem of how to allocate the things, as they are just values. This is obviously not fully equivalent to what you had before but may be close enough, depending on what your actual use case is.

C++ is really not much like C or Java, and if you try to imagine that it is, you're just going to headdesk until you stop.

Upvotes: 1

Ilya Kobelevskiy
Ilya Kobelevskiy

Reputation: 5345

As mentioned in the comments, you can use unique_ptr in a following way:

std::vector<std::unique_ptr<General>> build(char** things, int number) {
    std::vector<std::unique_ptr<General>> result;
    for(int i=0;i<number;i++) {
        if(strcmp(things[i],"A")) {
            result.push_back(std::unique_ptr<General>(new SpecificA()));
        } else if(strcmp(things[i],"B")) {
            result.push_back(std::unique_ptr<General>(new SpecificB()));
        }
    }
    return result;
}

Upvotes: 4

YePhIcK
YePhIcK

Reputation: 5856

You would need to create your objects on a heap and store the pointers in your collection. Like that:

std::vector<General*> build(char** things, int number) {
    std::vector<General*> result;
    for(int i=0;i<number;i++) {
        General* newObj = nullptr;
        if(strcmp(things[i],"A")) {
            newObj = new SpecificA();
        } else if(strcmp(things[i],"B")) {
            newObj = new SpecificB();
        }
        if(newObj != nullptr){
            result.push_back(newObj);  // you can do the "push" in one place
        }
    }
    return result;
}

Or, to use the std::unique_ptr:

std::vector< std::unique_ptr<General> > build(char** things, int number) {
    std::vector< std::unique_ptr<General> > result;
    for(int i=0;i<number;i++) {
        std::unique_ptr<General> newObj;
        if(strcmp(things[i],"A")) {
            newObj = std::unique_ptr<General>(new SpecificA());
        } else if(strcmp(things[i],"B")) {
            newObj = std::unique_ptr<General>(new SpecificB());
        }
        if(newObj.get() != nullptr){
            result.push_back(newObj);  // you can do the "push" in one place
        }
    }
    return result;
}

Upvotes: -1

Related Questions