Search code examples
c++design-patternscompiler-errorsstrategy-pattern

How the compiler infer types using strategy pattern (with overloaded() in struct or class)?


I am struggling to understand how this code even compile when passing a "callable" struct/class.

Referring to the main function below, I would expect the compiler to tell me that there is no matching constructor for Point3D (CallableI is not an instance of FuncType).

What I would be tempting to do is to create an algo member inside CallableI and constructs the point referring to it. But this solution is better on many aspect so I want to understand how it is working under the hood.

I have a base class Point3D :

template<typename T>
class Point3D
{
typedef std::function<T (const Point3D<T> &p1, const Point3D<T> &p2)> FuncType;
private:
    T x;
    T y;
    T z;
    FuncType algo;
public:
    Point3D(T x, T y, T z, FuncType algo) : x(x), y(y), z(z), algo(algo){}
    T First() const {return x;}
    T Second() const {return y;}
    T Third() const {return z;}
    
    T computeDistance(Point3D<T> &p2){
        return algo(*this, p2);
    }
};

I am using a struct CallableI to hold a particular function to be called overloading operator ().

struct CallableI
{
    CallableI(){};
    
    double operator() (const Point3D<double> &p1, const Point3D<double> &p2){
        double x1{p1.First()}; double x2{p2.First()};
        double y1{p1.Second()}; double y2{p2.Second()};
        return std::abs(x1 - x2) + std::abs(y1 - y2);
    }
};

And I am calling the suggested code with the main function below :

int main(int argc, char **argv) {
    
    CallableI myCallable;
    Point3D<double> p1(14.3, 12.3, 2.0, myCallable);
    return 0;

Solution

  • std::function is a type erasure template.

    std::function<R(Arg0, Arg1)> is a type erasure type.

    It can be constructed with any value that you can:

    1. Invoke with () with Arg0, Arg1 and the return type can be converted to R
    2. Can be copied, moved, and destroyed.

    It is a kind of polymorphism that does not require inheritance.

    Your CallballI fullfills the requirement, and the constructor of function<double(Point3D<double> const&,Point3D<double> const&)> does the work to make it work.

    How type erasure in C++ works is a more advanced subject. In std::function, how it works is unspecified; the standard just says it works. You can implement it yourself through a number of ways (which in turn could use inheritance), but at this point in your learning C++ I'd advise against it.

    The core idea is that C++ templates are ridiculously powerful, and std::function has a template constructor that write the glue code that lets you store an object of unknown but compatible type, and writes the code to invoke (Point3D<double>, Point3D<double>) on it.

    template<class Sig>
    struct function_view;
    
    template<class R, class...Args>
    struct function_view<R(Args...)> {
      void* pstate = nullptr;
      R(*pf)(void*, Args&&...) = nullptr;
    
      R operator()(Args... args) const {
        return pf(pstate, std::forward<Args>(args)...);
      }
    
      template<class T>
      function_view( T&& in ):
        pstate( (void*)std::addressof(in) ),
        pf( [](void* pvoid, Args&&...args )->R {
          return (*decltype(std::addressof(in))(pvoid))( std::forward<Args>(args)... );
        })
      {}
    };
    

    that is an incomplete (simplified in a number of ways) sketch of a semi-useful non-owning function view. This is not the only way to do type erasure in C++, just one that is brutal, simple and short.

    The core is that function_view<Sig> knows what it needs, it saves what how to do it when constructed and then forgets everything else about the type passed (it "erases" it from its memory).

    Then it uses those saved operations on the type erased data when you ask for it to do its operation.

    Live example.

    Here,

    void foo( function_view<int(int)> f ) {
      std::cout << f(3);
    }
    

    foo isn't a template, but can accept any unrelated type that supports calling with int and returns something that can convert to int.