Search code examples
c++opencvconstructor-overloadingnode-addon-api

C++ N-API Multiple Type Signatures


I am learning C++ and playing around with OpenCV and node-addon-api. I wanted to create my own wrapper for cv::Vec. docs

#include <napi.h>
#include <opencv2/core/matx.hpp>

class Vec : public Napi::ObjectWrap<Vec> {
public:
  static Napi::Object Init(Napi::Env env, Napi::Object exports);

  explicit Vec(const Napi::CallbackInfo &info);

private:
  static Napi::FunctionReference constructor;

  //
  // no type named 'Vec' in namespace 'cv';
  // I would like this to be cv::Vec2 or cv::Vec3 ... of any type
  cv::Vec *_wrappedClass_;

  // duplicate member '_wrappedClass_'
  // cv::Vec2 *_wrappedClass_;
  // cv::Vec3 *_wrappedClass_;
};

Of course the example above will not work because cv::Vec expects me to tell type and size. So something like this: cv::Vec<int, 3> would work and create a 3-dimensional vector.

My question is how do I properly overload the constructor and define the _wrappedClass_ type?

Should I create classes Vec2, Vec3, and so on that would extend the current Vec class?

When I looked at how more experienced developers dealt with the problem I found this example in opencv4nodejs. It seems more reasonable:

  • 1 cpp file
  • base header file
  • additional header files for class variants

I have full example on GitHub here.


Solution

  • Since you've mentioned you're learning C++, I've tried to give a little extra explanation to things other than just dumping an answer and assuming you know what I'm talking about. Any questions or clarifications let me know.

    My question is how do I properly overload the constructor and define the wrappedClass type?

    You do not overload the constructor. The arguments passed to the constructor are not known until run time, so they cannot be used to fill in the template arguments required which must be set at compile time in the cv::Vec class.

    From the doc for cv::Vec we can see that cv::Vec itself is a templated class.

    template<typename _Tp, int cn>
    class cv::Vec< _Tp, cn >
    

    This means to instantiate a cv::Vec you must provide both of those template arguments.

    C++ is strongly typed and template arguments are part of the type. This means a cv::Vec<int, 5> is not the same type as cv::Vec<int, 4> or cv::Vec<double, 5>. Like any other type in C++, templates must be deducible, fully formed, and set at compile time.

    This leads into your compilation error "no type named 'Vec' in namespace 'cv'" which is being emit because indeed there is no such thing as a cv::Vec with out any template arguments. There could be a cv::Vec<int,5> if you instantiated one. The compiler will not generate types for templates you do not use since. There is a fundamental principal of C++ which the compiler will follow: "What you don’t use, you don’t pay for [BS94]".

    You've probably noticed the code you linked does not seem to provide those arguments but instead uses something like cv::Vec6d. This is because they have pre-defined a few more common combinations as type aliases.. The use of the using keyword instead of typedef is the more modern idiomatic method of defining these aliases.

    Sticking with pure C++ decision making for a moment, you have two ways forward with this understanding. You can either define a new class for each combination of _Tp and cn you wish to support as the opencv4nodejs code has done that you linked, or you can template your Vec class.

    // with templates
    template< class ElementType, int Count >
    class Vec {
    private:
      cv::Vec<ElementType, Count> *_wrappedClass_;
    };
    

    This would allow you use to construct your own Vec classes of any type and size that you wish while writing the code once by instantiating things like Vec<double, 5> myVec; which would result in your class with a private member _wrappedClass_ that is a pointer to type cv::Vec<double, 5>.

    Unfortunately once we bring the node-addon-api requirement back in we are forced to consider further complications.

    Should I create classes Vec2, Vec3, and so on that would extend the current Vec class?

    Maybe. From the Napi ObjectWrap docs:

    At initialization time, the Napi::ObjectWrap::DefineClass() method must be used to hook up the accessor and method callbacks. It takes a list of property descriptors, which can be constructed via the various static methods on the base class.

    And then looking at the docs for DefineClass it does a little more than just link up accessors and method callbacks, it gives your object a name in the Javascript runtime via the second argument.

    [in] utf8name: Null-terminated string that represents the name of the JavaScript constructor function.

    This will need to be a different value for each combination of ElementType and Count in our above example. That name should not be shared between a Vec<double, 5> and Vec<double, 3> since, even if Napi did not throw an error, you wouldn't know which one you were getting in the Javascript.

    The name is fundamentally part of the type, but again you have options. You could go back to defining many Vec types again or you could pass this through templates arguments similar to how Count is.

    However, I have a feeling as you go further into this you're going to run into additional things that fully define this type. So rather than slowly expand the number of template arguments, we can have the realization that these arguments all exist to give minor adjustments to the behavior of the class. We can use an idiom in C++ called policy classes. In this way we can pull out the parts that dictate to the fundamental class into their own objects while leaving the fundamental class a single copy of the code.

    For example we might have:

    struct Vec2dPolicy {
      constexpr static char name[] = "Vec2d";
      constexpr static int  Count  = 2;
      using ElementType = double;
    };
    
    struct Vec3dPolicy {
      constexpr static char name[] = "Vec3d";
      constexpr static int  Count  = 3;
      using ElementType = double;
    };
    

    With the goal that we merely need to instantiate your class as:

    Vec<Vec2dPolicy> my2dvec;
    Vec<Vec3dPolicy> my3dvec;
    

    And have all fully defined variations of Vec that you need.

    So what does your Vec class look like? Ultimately it'll look something like:

    template< class Policy >
    class Vec : public Napi::ObjectWrap< Vec< Policy > > {
    public:
      Vec() {}
    
      template<class...Args, typename std::enable_if<sizeof...(Args) == Policy::Count, int>::type = 0>
      Vec(Args... args)
      : _wrappedClass_(args...)
      { }
    
      Napi::Object Init(Napi::Env env, Napi::Object exports) {
        Napi::Function func = DefineClass(
                                env,
                                Policy::name,
                                {
                                /* fill me in */
                                }
                              );
        /* fill me in */
      }
    
      // other member functions to fully define Napi object
    
    private:
      cv::Vec<typename Policy::ElementType, Policy::Count> _wrappedClass_ = {};
    };
    

    Note I took out the pointer to your private member _wrappedClass_ because I didn't understand why that would be a pointer.

    Tossed in there for fun is a way to define a constructor for Vec that takes Policy::Count arguments and passes them onto _wrappedClass_'s constructor. If they pass the wrong number of arguments it'll fail to compile as there is not a constructor defined. If the type of the arguments they pass in can't be converted to the correct type for _wrappedClass_'s constructor it'll also fail to compile.

    This is using a parameter pack along with the metafunction enable_if which takes advantage of SFINAE to ensure Vec(...) is only emit as code when the number of arguments matches the Policy::Count.

    Do note that cv::Vec does not define a constructor for every Count. They only define them for 0-10, and 14. With this setup, your class will define 11 but it'll fail to compile when passing 11 to _wrappedClass_ as it does not.

    So now you have the same functionality as if you had written a bunch of Vec2d Vec3d Vec4d and so on classes for each combination of dimension and underlying type, except you only have to write the core code once.

    Let me know if anything was unclear.