Reputation:
In the code below, I get a compiler error when trying to push fooBaz
onto v
. This surpises me since Baz
is a derived class of Bar
.
Why is this not allowed, and what can I do if want to put several Foo
instances, templated on classes derived from the same base class, into a vector?
#include <iostream>
#include <vector>
template<typename T>
class Foo {};
struct Bar {};
struct Baz : public Bar {};
int main() {
Foo<Bar> fooBar;
Foo<Baz> fooBaz;
std::vector<Foo<Bar>> v;
v.push_back(fooBar);
v.push_back(fooBaz);
return 0;
}
Upvotes: 0
Views: 439
Reputation: 275385
Java Generics are not the same kind of thing as C++ templates.
C++ values of class type are not the same thing as Java reference variables of class type.
You are running into both problems here.
C++ templates generate a new, unrelated type for each set of template arguments. You can create a common base, but you have to do it yourself.
Java Generics, under the hood, actually create a single class. It then writes casting operations at inputs and outputs.
So a Java Generic, Foo<Base>
and Foo<Derived>
are related, because the the Java Generic actually creates a Foo<Object>
then wraps it up in casts, and those casts in Foo<Base>
and Foo<Derived>
are compatible. (well, not always Object
, you mark up the generic arguments with information that Java uses to determine what the actual type it writes its Generic for, but that gives you the idea).
In C++, there is no relation. (well, template pattern matching gives you a compile-time relation, but no runtime relation at all)
The second problem is that you are treating values of class type like references. In C++, a Foo
is an actual foo. It represents a block of memory that is an instance of that class. In Java, a Foo
is a smart pointer to an object on the heap somewhere that obeys the Foo
protocol (is a derived class).
You cannot easily make a value of type Foo
in Java, and you cannot easily make a mark and sweep smart pointer to a Foo
in C++.
Foo<Bar> fooBar;
Foo<Baz> fooBaz;
these are two unrelated types. They are stored on the stack (automatic storage).
std::vector<Foo<Bar>> v;
This stores a buffer of memory containing Foo<Bar>
objects packed together.
v.push_back(fooBar);
This copies a fooBar
instance from automatic storage into the vector
.
v.push_back(fooBaz);
This doesn't work, because fooBar
and fooBaz
are unrelated types.
Now, prior to c++23 reflection, mimicing what Java does is difficult in C++. You have to do some steps manually.
First, instruct Foo
to understand inheritance when told so manually:
struct empty_t {};
template<class T, class Base=empty_t>
class Foo:Foo<Base> {};
template<>
class Foo<empty_t, empty_t> {
virtual ~Foo() {}
};
struct Bar {};
struct Baz : public Bar {};
auto fooBar = std::make_unique<Foo<Bar>>();
auto fooBaz = std::make_unique<Foo<Baz, Bar>>();
std::vector<std::unique_ptr<Foo<Bar>>> v;
v.push_back(std::move(fooBar));
v.push_back(std::move(fooBaz));
this compiles.
In c++23 compile time reflection should let you auto-detect the base classes of Baz
and have Foo<Baz>
automatically inherit from Foo<Bases>...
if you want.
Now, inheritance is only one kind of way to handle polymorphism in C++, but I think is enough for today.
Upvotes: 1
Reputation: 122298
As mentioned in a comment, templates are like recipes. You need to instantiate a class template to get a class, before it is just a template. There is no implicit relation between different instantiations (other than being instantiations of the same template). Maybe this gets more clear when you consider following example:
template<typename T> struct Foo {};
template <> struct Foo<int> { void bar(){} };
template <> struct Foo<double> { void moo(){} };
int main() {
Foo<int> x;
x.bar();
Foo<double> y;
y.moo();
}
Foo<int>
and Foo<double>
are two unrelated types with completely different methods. If they were not instantiations of a template, but "ordinary" types with different members you would not be surprised that you cannot push a FooA
into a std::vector<FooB>
.
A std::vector<Foo<Bar>>
can only hold elements of type Foo<Bar>
. The vector is not aware that this type is the result of instantiating the template Foo
. And even if it was, you cannot assign a Foo<Bar>
to a Foo<Baz>
unless you provide a conversion.
Actually there is more to your misunderstanding that needs to be debunked. Suppose you have a std::vector<Bar>
then you also cannot push a Baz
to that vector. See here for why not: What is object slicing?. And here for what to do instead: https://stackoverflow.com/a/16126649/4117728.
Upvotes: 0