Kai Salmon
Kai Salmon

Reputation: 365

C++ Generics and Polymorphism: is this pattern workable?

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 Names which extend Words. 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

Answers (3)

Sneftel
Sneftel

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

user7860670
user7860670

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

online compiler

Upvotes: 0

rustyx
rustyx

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:

  • avoid copying objects unnecessarily by passing them by reference.
  • to reduce memory leaks avoid operator new and use std::list<std::unique_ptr<Word>> and std::make_unique instead.

Upvotes: 1

Related Questions