1

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.

Michal
  • 4,952
  • 8
  • 30
  • 63

2 Answers2

1

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.

Thomas Mouton
  • 473
  • 4
  • 7
  • I am so happy that we live in a world where people not only can share their knowledge but where people care enough to spend time writing something as detailed as you did. – Michal Dec 02 '21 at 18:18
  • I have one question though? I understand templates and everything that you did. I was unable to initialize the class from your example `Vec::Init(env, exports);`. The `Init` method is usually static. I will update my question in a second. – Michal Dec 02 '21 at 18:34
  • 1
    I just updated the answer. I am going to be playing around with it for a while. – Michal Dec 02 '21 at 18:47
  • I can accept this answer and create another question if the answer is more complicated and would be worth documenting for other developers. – Michal Dec 02 '21 at 18:50
  • @Michal ah I was trying to directly answer the question and show how you might do certain things which I admit is kind of muddied when combining it with actually implementing your full class. – Thomas Mouton Dec 02 '21 at 19:09
  • 1
    Give me a few hours. I think editing my answer rather than asking a new question is probably best to continue helping you. Please post your code as it is now at the bottom of your question and update it as you make progress on your own. Briefly: * The first error you posted is likely because you're not fully qualifying the base class's name. Remember you'll need to include the template arguments. * The second error is because the constructors in my example were kind of for illustrative purposes. You'll only want a single constructor that matches the example in the Napi docs. – Thomas Mouton Dec 02 '21 at 19:17
  • Take your time. I am playing around with templates I will update the question in an hour or so. – Michal Dec 02 '21 at 19:19
  • 1
    I think I got it I will update my question tomorrow latest and I will probably create and then answer my own question tomorrow. In short the main problem was with C++ template class inherit. – Michal Dec 04 '21 at 18:53
0

So from what @thomasMouton write in his accepted question, I put together something like this.

We can define policies.

struct Vec2dPolicy {
  constexpr static char *name = "Vec2";
  constexpr static int  Count  = 2;
  using ElementType = double;
};

struct Vec3dPolicy {
  constexpr static char *name = "Vec3";
  constexpr static int  Count  = 3;
  using ElementType = double;
};

Then we initiate our classes like we usually do.

Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
  Vec<Vec2dPolicy>::Init(env, exports);
  Vec<Vec3dPolicy>::Init(env, exports);

  return exports;
}

Header file for our variant can look like this:

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

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

private:
  static Napi::FunctionReference constructor;

  Napi::Value getX(const Napi::CallbackInfo &info);
  Napi::Value getY(const Napi::CallbackInfo &info);
  Napi::Value getZ(const Napi::CallbackInfo &info);
};

And then the actual implementation:

template<class VariantPolicy>
Napi::Value Vec<VariantPolicy>::getZ(const Napi::CallbackInfo &info) {
  Napi::Env env = info.Env();

  return Napi::Number::New(env, 3);
}

template<class VariantPolicy>
Napi::Value Vec<VariantPolicy>::getY(const Napi::CallbackInfo &info) {
  Napi::Env env = info.Env();

  return Napi::Number::New(env, 2);
}

template<class VariantPolicy>
Napi::Value Vec<VariantPolicy>::getX(const Napi::CallbackInfo &info) {
  Napi::Env env = info.Env();

  return Napi::Number::New(env, 1);
}

template<class VariantPolicy>
Vec<VariantPolicy>::Vec(const Napi::CallbackInfo &info) : Napi::ObjectWrap<Vec<VariantPolicy>>(info) {
  Napi::Env env = info.Env();
  Napi::HandleScope scope(env);
}

template<class VariantPolicy>
Napi::Object Vec<VariantPolicy>::Init(Napi::Env env, Napi::Object exports) {
  Napi::HandleScope scope(env);

  Napi::Function func = Napi::ObjectWrap<Vec<VariantPolicy>>::DefineClass(env, VariantPolicy::name, {
      Napi::ObjectWrap<Vec<VariantPolicy>>::InstanceAccessor("x", &Vec<VariantPolicy>::getX, nullptr),
      Napi::ObjectWrap<Vec<VariantPolicy>>::InstanceAccessor("y", &Vec<VariantPolicy>::getY, nullptr),
      Napi::ObjectWrap<Vec<VariantPolicy>>::InstanceAccessor("z", &Vec<VariantPolicy>::getZ, nullptr),
  });

  constructor = Napi::Persistent(func);
  constructor.SuppressDestruct();

  exports.Set(VariantPolicy::name, func);
  return exports;
}

template<class VariantPolicy>
Napi::FunctionReference Vec<VariantPolicy>::constructor;

This is of course simplified version but it answers the question.

Michal
  • 4,952
  • 8
  • 30
  • 63