0

I'm currently trying my hand at a project on classes and libraries. Imagine I have the following classes:

  • Virtual Class "Animal" as an Interface as a header file
  • An expandable number of child classes (Dog, Cat, ...), each as separate files
  • A main file in which every existing child of "Animal" gets instanciated, added to a vector and i can loop through the functions specified in the Parent-Class

As an example of the above. The interface "animal.hpp":

class Animal
{
    public:
    virtual void eat() = 0;
};

One of the implemented creatures (dog.cpp). These should be expandable simply by adding another .cpp-file which are childs of "Animals".

class Dog : public Animal
{
    void eat() override
    {
        std::cout << "Dog eats..." << std::endl;
    }
};

And inside the main project something like this. But I don't want to edit this file in order to add new instances of an creature to the vector. (They can all be compiled together but if at runtime is also possible, that would be also interesting.)

int main(int argc, char const *argv[])
{
    std::vector<Animal*> creatures;
    creatures.push_back(new Dog);
    creatures.push_back(new Cat);
    for (auto i : creatures)
    {
        i->eat();
    }
    return 0;
}

Is this even possible? If so, can someone pinpoint me into the right direction or give me some keywords to search for?

I already tried using the Factory Design pattern but to no success. I stumble over new header files that I would have to include. And in this example (see above) I would also have to add the instances manually.

Davider
  • 11
  • 4
  • What you're trying to accomplish would require something like reflection (as in C#). C++ does not support such a thing. Somewhere you need to have code that creates the instance and adds it to the vector. You could think of alternative solutions. Not saying this is the right design though. For example: have a global creatures vector somewhere. And in each .cpp file, globally create an instance of the class of that .cpp file and add it to the global creatures vector. – Tohnmeister May 04 '23 at 11:10
  • 2
    It's relatively simple for almost any scripting language to go through a set of files (fetched with globbing) and find if they contain text corresponding to `class X : public Animal`, extract the `X` (regular expression could help), and then automatically create a source file that contains a function which creates your vector. But *why*? What is the original and underlying problem this is supposed to solve? Why create this vector or array at compile-time? What is your assignment? What are its requirements? Its limitations? – Some programmer dude May 04 '23 at 11:16
  • "Just adding a file" won't work in C++ as at some point you have to specify to pick it up for compilation. I mean, maybe a homespun makefile that wildcards it, but I would recommend against that. **But** you can get most of the way there with a self-registering factory. – sweenish May 04 '23 at 11:28
  • Sounds you want a kind of _Registry-Factory_ with _Prototype Factory_ instances. – πάντα ῥεῖ May 04 '23 at 11:36
  • I've once asked a [very low rated question](https://stackoverflow.com/questions/44465134/how-to-self-register-class-instances-using-the-crtp), which may point out what I mean. – πάντα ῥεῖ May 04 '23 at 11:38
  • @Someprogrammerdude I would like to allow third parties (e.g. other programmers) to add new "animals" that are also considered by the main code without having to make changes in the main program. Since I posted the question I have come across "plugins" and that sounds like about what I was looking for. I'll try to implement something like that, though it looks quite complex. – Davider May 04 '23 at 11:42
  • Plugins, DLL's, shared objects. It has many names but most platforms have some kind of dynamically loadable objects that could do what you want. Create a consistent API, with a factory function that creates the wanted object (returning it as a pointer to the abstract base class of course). Then if properly set up that's really all you need. There are plenty of tutorials on how to do this for all major systems (Windows, Linux, macOS). Try out some of them, and please come back for new questions when you need. :) – Some programmer dude May 04 '23 at 12:13
  • Oh by the way, while there are many good tutorials, some of them tend to forget one crucial part to make it work in an easy way: To "export" the factory function in an easy way, it needs to be using C-language linkage. I.e. be declared/defined with `extern "C"`. Otherwise you need to know the compiler-specific [*name mangling*](https://en.wikipedia.org/wiki/Name_mangling) scheme to find the function in the "plugin". It's only the publicly available API that needs C-language linking. – Some programmer dude May 04 '23 at 12:15
  • Thank you all for your imput! I'll comment/reply as soon as I made some progress. – Davider May 04 '23 at 15:05

3 Answers3

0

You might call code via global initialization

struct Dog : public Animal {/**/};

static bool dummy = (AddToGlobalVector(make_unique<Dog>()), true);

in main:

std::vector<std::unique_ptr<Animal>>& creatures()
{
    static std::vector<std::unique_ptr<Animal>> animals;
    return animals;
}

void AddToGlobalVector(std::unique_ptr<Animal> animal) {
    creatures().emplace_back(std::move(animal));
}

int main()
{
    for (auto& i : creatures())
    {
        i->eat();
    }
    return 0;
}
Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • 1
    This isn't guaranteed to work on all compilers. I'm not sure what the spec says but it's known that MSVC will discard object files which are not referenced. There is a work-around for this which I can't recall. See https://social.msdn.microsoft.com/Forums/vstudio/en-US/3d31eba5-7713-45e7-bffe-2b420968642e/does-msvc-linker-discard-global-objects?forum=vclanguage – Mike Vine May 04 '23 at 11:19
0

In addition to Jarod42's answer (any my comment). I don't fully understand why the factory pattern was a problem. Assuming the only requirement would be to not have the instances created in the main function but somewhere else, you could simply create an animal_factory.h which holds the following code:

#pragma once

#include <vector>

class Animal;

class AnimalFactory
{
public:
    static std::vector<Animal*> create_animals();
};

And then a animal_factory.cpp file which holds the implementation:

#include "animal_factory.h"
#include "dog.h"
#include "cat.h"
#include "mouse.h"

std::vector<Animal*> AnimalFactory::create_animals()
{
    return { new Dog, new Cat, new Mouse };
}

Don't even have to wrap it in a class. create_animals could be a standalone function defined somewhere else than in the main file.

Tohnmeister
  • 468
  • 1
  • 5
  • 14
0

I've implemented a simple plugin system in Ubuntu. That was exactly what I was looking for. Thanks to @Some programmer dude for the hint about "extern "c".

The implementation

Project structure

src
  main.cpp
  cat.cpp
  dog.cpp
include
  animal_interface.hpp
plugins
  cat.so  (after compilation)
  dog.so  (after compilation)
main.exe (after compilation)

main.cpp

#include <iostream>
#include <vector>
#include <dlfcn.h>
#include <dirent.h>
#include "../include/animal_interface.hpp"

const char* path = "./plugins";

int main() {
    // Load the plugins
    std::vector<void*> plugin_handles;
    std::vector<PluginInterface*> plugins;
    DIR* dir = opendir(path);
    if (dir) {
        struct dirent* entry;   //dirent = directory entry
        while ((entry = readdir(dir)) != NULL) {
            if (entry->d_type == DT_REG) {
                std::string filename = std::string("./plugins/") + entry->d_name;
                void* handle = dlopen(filename.c_str(), RTLD_NOW);
                if (handle) {
                    plugin_handles.push_back(handle);
                    typedef PluginInterface* (*create_plugin_t)();
                    create_plugin_t create_plugin = reinterpret_cast<create_plugin_t>(dlsym(handle, "create_plugin"));
                    if (create_plugin) {
                        PluginInterface* plugin = create_plugin();
                        plugins.push_back(plugin);
                    } else {
                        std::cerr << "Error loading symbol: " << dlerror() << std::endl;
                    }
                } else {
                    std::cerr << "Error loading plugin: " << dlerror() << std::endl;
                }
            }
        }
        closedir(dir);
    }
    else
    {
        std::cerr << "No such folder" << std::endl;
    }

    for(auto animal : plugins){
        animal->eat();

    }

    // Clean up
    for (auto plugin : plugins) {
        delete plugin;
    }
    for (auto handle : plugin_handles) {
        dlclose(handle);
    }

    return 0;
}

animal_interface.hpp

#pragma once

class PluginInterface {
public:
    virtual void eat() = 0;
    virtual ~PluginInterface() {}
};

cat.cpp

#include <iostream>
#include "../include/animal_interface.hpp"

class cat : public PluginInterface {
public:
    void eat() override {
        std::cout << "Cat is eating" << std::endl;
    }
};

extern "C" PluginInterface* create_plugin() {
    return new cat();
}

dog.cpp is exactly the same with different names.

Compiling

You can compile the files with:

g++ src/main.cpp -ldl -o main.exe
g++ -shared -fPIC src/cat.cpp -o plugins/cat.so
...

Results

If you execute main.exe the output should be depended if or which shared objects are present. If only the cat-object is inside the plugins folder, then the output should be

Cat is eating
Davider
  • 11
  • 4