Reputation: 365
I understand how Polymorphism and Generics interact in other programming languages (Java, C#, Typescript, ect.). In C++ however it feels like a pattern I would like to utilize fails.
In this example I want to have a list of Name
s which extend Word
s. I want to pass my list of names into a method which accepts a list of words, but I cannot. I can populate a list of words with my names, this however loses the type information, meaning I cannot call any methods inherit to the Name class.
#include <iostream>
#include <string>
#include <list>
class Word{
public:
virtual void say() = 0;
};
class Name : public Word{
std::string name;
public:
Name(std::string name){
this-> name = name;
}
void say() override{
std::cout << name << std::endl;
}
void important_name_function(){
// Something very important I want to call
}
};
void say_one(Word* w){
w-> say();
}
void say_all(std::list<Word*> list){
for(Word* w: list){
w-> say();
}
}
int main(){
std::list<Word*> words = {new Name("Kai"), new Name("Ben"), new Name("Sam")};
say_one(words.front()); //Works, due to the magic of polymorphism
say_all(words); //Works, due to the magic of polymorphism
std::list<Name*> names = {new Name("Kai"), new Name("Ben"), new Name("Sam")};
say_one(names.front()); //STILL works due to the magic of polymorphism AND type information is retained
say_all(names); //Fails but feels like it shouldn't
}
In, for example, Java I would be able to solve this issue by defining say all as
static <T extends Word> void say_all (java.util.LinkedList<T> list){
for(T w:list){
w.say();
}
}
However, looking for this solution in C++ gets what to my eyes looks like an ugly solution (C++ equivalent of using <T extends Class> for a java parameter/return type)
To me this means that one of the following is true:
Upvotes: 1
Views: 100
Reputation: 41503
You're not wrong -- this is something C++ isn't great at. It doesn't currently have an equivalent to Java's bounded type parameters, meaning that if you want that specific level of control over what say_all
can take, rather than just doing template<typename T> void say_all(list<T> const& l)
(or even template<typename T> void say_all(T const& l)
) and counting on the internal usage to throw errors, you'll need to do that manually, with enable_if
and friends.
This is something that the maybe-upcoming C++ "concepts" feature is intended to address:
template<typename T> requires DerivedFrom<T, Word> void say_all(list<T> const& l) { ...
(Note that syntax and standard library support is still subject to change).
Still, in this case that's just in service of a guaranteed, early, and easy-to-troubleshoot compiler error if you try to pass a list of something else in. Honestly, my approach here would probably be to just document that say_all
expects a list of something subclassing Name
, and rely on a probable compiler error if that gets violated.
Upvotes: 0
Reputation: 37600
You should be able to implement a generic function similar to java one using ::std::is_base_of
type trait:
template
<
typename x_Word
, typename x_Enabled = ::std::enable_if_t
<
::std::is_base_of_v<Word, x_Word>
>
>
auto
say_all(::std::list<x_Word *> & words) -> void
{
for(auto & p_w: words)
{
p_w->say();
}
return;
}
Upvotes: 0
Reputation: 85452
- I am incorrectly assessing it as ugly
That.
I don't find the following ugly:
template<class T>
void say_all(const std::list<T*>& list) {
for (T* w : list) {
w->say();
}
}
Note that you don't have to restrict T
at all in your example. Can't really match that in Java.
Only if you actually need to restrict T
to an instance of Word
:
template<class T, typename = std::enable_if_t<std::is_base_of<Word, T>::value>>
void say_all(const std::list<T*>& list) {
for (T* w : list) {
w->say();
}
}
Or with concepts:
template<typename T>
concept IsWord = std::is_base_of<Word, T>::value;
template<class T> requires IsWord<T>
void say_all(const std::list<T*>& list) {
for(T* w : list) {
w->say();
}
}
Side notes:
new
and use std::list<std::unique_ptr<Word>>
and std::make_unique
instead.Upvotes: 1