c++ooptemplatesmathematical-optimizationheuristics

How do I create an abstract class inside a parent class so that every children class can have it's own implementation?


First of all, I recently made the jump from Julia to C++, so I'm quite new to some of these concepts, so sorry if I don't use the correct terminology. I'm implementing a metaheuristic in C++, and I'm trying to come up with an abstract class that each neighborhood will have to implement. Every neighborhood has it's own arguments, which correspond to indexes to the solution vector they'll try to change/update, some use only 1 argument, some use many, and every neighborhood has it's own evaluation method, which return the loss/gain prompted by the changes of the arguments, and a move method that does the changes.

This is what I have in mind, but I'm not sure how to complete it, and if I'm on the right track. Should I use templates, if so, how? Thanks!

class Neighborhood
{
public:
    class NeighborhoodArgs
    {

    };

    virtual void move(NeighborhoodArgs args);
    virtual double evaluate(NeighborhoodArgs args);

private:
    Data& data;
    Parameters& parameters;
};
#include "Neighborhood.h"

class TwoOptNeighborhood : Neighborhood
{
public:
    class TwoOptArgs : NeighborhoodArgs
    {
        int pos1;
        int pos2;
    };

    void move(TwoOptArgs args) override;
    double evaluate(TwoOptArgs args) override;
};

Solution

  • My current understanding of the question: you have a bunch of classes that have the same "general logic" and you want to apply it in uniform ways.

    struct A {
      struct Args {
        //(something A-unique here)
      };
      void move(Args x); //A-unique implementation
      double evaluate(Args x);
    };
    struct B {
      struct Args {
        //(something B-unique here)
      };
      void move(Args x);
      double evaluate(Args x);
    };
    //5 more classes like this
    //want to:
    for(size_t i = 0; i < solutions.size(); ++i){
      Type T = solution[i].Type();
      T::Args x;
      x.ReadFrom(solution[i]);
      T calc;
      calc.move(x);
      calc.evaluate(x);
    }
    

    If this logic can be expressed in terms of invariant types, the solution would be to find operations with identical signatures (making no mention of specific types, only of their interfaces). Like so:

    struct IArg {
      virtual double Value1(void) const = 0;
      virtual double Value2(void) const = 0;
      virtual void ReadFrom(const Solution&) = 0;
      virtual ~IArg() = default;
    };
    
    struct ICalc {
      virtual void move(IArg& x) = 0;
      virtual double evaluate(IArg& x) = 0;
      virtual std::unique_ptr<IArg> createArg(void);
      void Perform(Solution& s){
        //universal logic, in terms of ICalc / IArg
        ICalc* calc = s.solver;
        auto x = calc->createArg();
        x->ReadFrom(s);
        calc->move(*x);
        calc->evaluate(*x);
      }
      virtual ~ICalc() = default;
    };
    
    struct A : public ICalc {
      struct Args : public IArg {
        //...
      };
      virtual void move(IArg& x) override; //implemented ONLY in terms of IArg, not A::Args
      virtual double evaluate(IArg& x) override;
      virtual std::unique_ptr<IArg> createArg(void){return std::make_unique<Arg>();}
    };
    //...
    for(size_t i = 0; i < solutions.size(); ++i){
      //Obtain specific instance of ICalc for the task somehow
      ICalc* calc = solutions[i].solver;
      //Invoke universal logic
      calc->Perform(solutions[i]);
    }
    

    Assuming this doesn't work out well (structs with different numbers of fields are hard to unite under one interface)... templates to the rescue! CRTP relies on different types having things with the same names and similar meaning:

    struct BaseCalc {
      virtual void Perform(Solution& s) = 0;
      virtual ~BaseCalc() = default;
      //common data goes here
    };
    //Worker MUST declare Worker::Args, void move(Args), double evaluate(Args)
    template<typename Worker>
    struct Calc : public BaseCalc {
      void Perform(Solution& s) override {
        //universal logic, relying on universal names
        typename Worker::Args x;
        x.ReadFrom(s);
        move(x);
        evaluate(x);
      }
    };
    struct A : public Calc<A> { //generates Calc<A>::Perform()
      //the rest is the same, no virtual functions required
    };
    struct B : public Calc<B> {
      //...
    };
    //...
    for(size_t i = 0; i < solutions.size(); ++i){
       //Obtain specific instance of BaseCalc somehow
       BaseCalc* calc = solutions[i].solver;
       //Invoke universal logic
       calc->Perform(solutions[i]);
    }