Reputation: 7611
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,
struct
s (such as classes) should be passed by reference rather than copied around and returned from functionsIt'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
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:
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
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
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
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