简体   繁体   中英

Inheritance and template parameter

I have a rather simple question about template parameters. I'm writing a class Scheduler which uses a list as follows:

list<PartitionT<CompareJobReady, CompareJobRunning> > partitions;

PartitionT is a templated class which used priority_queues, and I want to parametrise these queues using comparator classes. CompareJobReady and CompareJobRunning are theses classes (they implement a specific operator()).

Anyway, as PartitionT is a templated class, I want to be able to pass any type of comparator class. In particular, I've defined two additionnal classes, namely CompareJobReadyEDFVD (which inherits from CompareJobReady) and CompareJobRunningEDFVD (which inherits from CompareJobRunning).

What I actually want to do now, is being able to write something like this:

list<PartitionT<CompareJobReady, CompareJobRunning> > *p = new list<PartitionT<CompareJobReadyEDFVD, CompareJobRunningEDFVD> >();

But the compiler tells me the conversion is not possible. What's the best solution to my problem?

The container classes of the Standard library own their elements. That is, if you have:

class Base {};
class Child : public Base {};

std::list < Base > myList;
std::list < Base > *pMyList;

Then the elements of myList can be accessed only as (references to) objects of type Base . You can store elements of type Base (eg push_back , emplace_back ) and get references / copies of (eg front , or via iterators), for example, see cppreference/list . Let's take a look at push_back :

void push_back(const value_type&);

Where value_type is the first template parameter you passed to std::list . In your case, that is PartitionT < CompareJobReady, CompareJobRunning > , or in the upper example, it's Base . push_back actually copies the argument you pass and makes that copy a new element. Why is that? Because the new element can be owned by the list. The list can destroy this element when the list is destroyed itself, and pass it on to another list if you move/swap the two lists. If the element had not been copied and been destroyed from outside, the list would contain a destroyed element - this would break the guarantees given by the list (and it wouldn't be very nice).

Another example (simplified, not very accurate): list allocates memory for its elements via an allocator. The default allocator here is std::allocator<Base> . It only allocates as much memory for the element as is required to store an object of type Base .

When you do something like pMyList = new std::list < Child >; , the right side results in a "pointer to std::list<Child> " whereas the left side is of type "pointer to std::list<Base> ". These two types are unrelated, as std::list<Base> does not inherit from or defines a conversion to std::list<Child> . There are some reasons why this is rather good, one being that generic programming requires to know what types it deals with. Polymorphism is an abstraction so that you don't have to - and cannot - know at compile time, what types you're dealing with.

Polymorphism in C++ works via pointers or references. So if you want to arrange some Child objects in a list that is agnostic of this type (eg it only knows the Base type), you have to store them as pointers:

std::list < std::shared_ptr<Base> > myList2;

myList2.push_back( new Child );    // better not use, there's a caveat (1)

// approach w/o this caveat
std::shared_ptr<Base> pNewChild( new Child );    // or make_shared
myList2.push_back( pNewChild );

Note I use a shared_ptr here, you can as well use a unique_ptr if that fits you purposes better, but you shouldn't use raw ptrs: As this myList2 owns its shared_ptr elements and a shared_ptr holds an object (of type Base) in shared ownership, myList2 indirectly owns the Child objects you store ptrs of in the list. As raw ptrs do not express ownership, it's for example not obvious who's responsible for destroying them. Read more about "Rule of Zero" .

(1) There is a caveat, see boost though it doesn't affect this example.


If you really want to do generic programming by selecting somewhere the Comparer classes, you "have to stick to compile time": Your list type (type of *p ) shouldn't be fixed to list<Base> but rather generic (using templates) and all the algorithms you use upon it have to be generic, too. You cannot (simply*) mix generic programming and a run-time selection of types, as generic programming is all about creating code at compile time.

*There's a hack that allows it, abusing RTTI and therefore very slow.

Use templating for the types, but inheritance for behaviour. Provide a constructor into which you can pass constructed comparator classes (as templated argument types), like

list<PartitionT<CompareJobReady, CompareJobRunning> > *p = 
  new list<PartitionT<CompareJobReady, CompareJobRunning> >( 
    new CompareJobReadyEDFVD(), new CompareJobRunningEDFVD());

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM