jeudi 13 août 2015

Referring to an object of a derived class from the base class of another (unrelated!) class in C++

If you can't understand the question title from the onset, it's not your fault - I couldn't think of a better description. Here is the explanation of the problem, which might be a bit lengthy, so apologies in advance.

In the initial version of my program, I had an Ecosystem class and an Individual class:

// Very simplified, for illustration purposes

class Ecosystem
{
    protected:
        // The int is just the ID of the individual.
        std::map<int, std::shared_ptr<Individual> > individuals;

    public:
        Ecosystem();
        void func(int _individual_id)
        {
            std::cout << "Individual's age: " 
                      << individuals[_individual_id]->get_age() 
                      << std::endl;
        }

        void routine(int _individual_id)
        {
            // Another function working via
            // the pointers in individuals.
        }        

        // More such functions...
};

class Individual
{
    protected:
        int age;

    public:
        Individual();
        inline int get_age() const
        {
            return age;
        }
};

The Ecosystem class contains dozens of functions, and I will add a lot more in the future.

I have now decided to split the Individual class into a base class and two derived classes, say TypeAIndividual and TypeBIndividual, because they each have members and attributes that the other one does not need (they also share a few members and attributes via the base class). So I have the base Individual class and two derived classes:

class TypeAIndividual : public Individual
{
    protected:
        // Data structures specific to individuals of type A

    public:
        TypeAIndividual();
};

class TypeBIndividual : public Individual
{
    protected:
        // Data structures specific to individuals of type B

    public:
        TypeBIndividual();
};

The problem is that the ecosystem now also needs to be split into TypeAEcosystem and TypeBEcosystem:

class Ecosystem
{
    protected:
        // Holding pointers to the base Individual class is pointless (pun not intended)
        // std::map<int, std::shared_ptr<Individual> > individuals;

    public:
        Ecosystem();
        // I want to keep func() in the base class
        // because it only accesses attributes and
        // members common to both classes derived
        // from Individual. 
        void func(int _individual_id)
        {
            // Hmmmm...
            // The pointers don't live in the Ecosystem class any more!
            std::cout << "Individual's age: " 
                      << individuals[_individual_id]->get_age() 
                      << std::endl; 
        }
        // OK to implement in each class
        // derived from Ecosystem.
        virtual void routine(int _individual_id) = 0;
};

class TypeAEcosystem : public Ecosystem
{    
    protected:
        // Pointers to individuals
        // of the corresponding type.
        std::map<int, std::shared_ptr<TypeAIndividual> > individuals;

    public:
        TypeAEcosystem();
        // Reimplementing routine() is OK
        // because it does things specific to
        // this individual type.
        virtual void routine (int _individual_id)
        {
            // Operate on data structures particular
            // to this type of individual.
        }

};

class TypeBEcosystem : public Ecosystem
{
    protected:
        // Pointers to individuals
        // of the corresponding type.
        std::map<int, std::shared_ptr<TypeBIndividual> > individuals;

    public:
        TypeBEcosystem();
        // Reimplementing routine() is OK
        // because it does things specific to
        // this individual type.
        virtual void routine (int _individual_id)
        {
            // Operate on data structures particular
            // to this type of individual.
        }
};

TypeAEcosystem and TypeBEcosystem both use void func(int _individual_id), which needs to access individuals of the corresponding type. But the base class Ecosystem doesn't contain pointers to individuals any more because the std::maps are in each derived class and not in the base class.

My question is: how can I access the appropriate type of individual (TypeAIndividual or TypeBIndividual) while avoiding implementing separate void func(int _individual_id) in each class derived from Ecosystem? In other words, is there a way to keep func() in the base class so that when I change it, I don't have to make changes to the derived classes? In the actual program, there are dozens of functions like func() which take just an int as a parameter. Also, some of those functions take individual IDs from other structures in the Ecosystem class, so I can't simply pass a pointer to TypeAIndividual or TypeBIndividual.

Things I have considered

  • Merging TypeAIndividual and TypeBIndividual back into a common Individual class with all the data structures necessary for both derived classes. This strikes me as a particularly clumsy way of doing things, but at least it will work.

  • Making func() & Co. virtual and implementing them in TypeAEcosystem and TypeBEcosystem. This means that if I want to make a change in any of the functions, I have to change both implementations (= a maintenance nightmare).

  • Having only one Ecosystem class which holds std::maps of the two types of individuals, like this:

    // Seems clunky...
    class Ecosystem
    {
        protected:
            // Note: The Ecosystem can contain 
            // one OR the other, but not both!
            // One map will always be empty.
        std::map<int, std::shared_ptr<TypeAIndividual> > type_a_individuals;
    
        std::map<int, std::shared_ptr<TypeBIndividual> > type_b_individuals;
    
        public:
            Ecosystem();
            void func(int _individual_id)
            {
                // Check what type of individuals we 
                // are working with and operate on the
                // appropriate container.
                if (type_a_individuals.size() > 0)
                {
                    std::cout << "Individual's age: " 
                              << type_a_individuals[_individual_id]->get_age() 
                              << std::endl; 
                }
                else
                {
                    std::cout << "Individual's age: " 
                              << type_b_individuals[_individual_id]->get_age() 
                              << std::endl; 
                }
            }
    };
    
    

This would require inserting a check in every function, which is almost as bad in terms of maintainability as having the functions in separate classes.

Note: Although I would very much like to avoid passing pointers around, I would consider upcasting and/or downcasting as appropriate (as a last resort...) if it solves the problem.

Any suggestions are welcome!


Edit 1

Thank you all for the fantastic responses! As suggested by both amit and Chris, and looked at my Ecosystem class and sure enough, it was too bulky. I moved member functions around into other classes and now I'm down to four or five essential functions in the Ecosystem class. The Ecosystem class resides in a library and provides an interface for conducting experiments with individuals, but I don't want users to be able to manipulate Individuals and other classes directly, so I can't do away with it completely.

I liked all suggestions, there are some ingenious solutions. That being said, the one proposed by Chris grabbed my attention immediately for being very neat and allowing me to have a single Ecosystem class rather than three separate classes (base and two derived). The type of individual can be specified in a config file, and I can spawn multiple ecosystems from different config files within the same experiment. This is the accepted answer.

Thank you again everyone for the constructive input!



via Chebli Mohamed

Aucun commentaire:

Enregistrer un commentaire