user6429576
user6429576

Reputation:

Adding instances of a template class to a vector (C++)

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

Answers (2)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

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 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 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

463035818_is_not_an_ai
463035818_is_not_an_ai

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

Related Questions