Adi Shavit
Adi Shavit

Reputation: 17265

Parametrized Operator Overloading

My library has a several operations that naturally map to arithmetic and logic/boolean operators. However, these operations require additional parameters besides lhs and rhs.

Here is a geometric example of what I mean demonstrated with ClipperLib:
Let's say I want to create a polygon offset operator that takes a 2D polygon and an offset delta and performs polygon offsetting. This example is from ClipperLib: enter image description here
Mathematically, this operation could be mapped to an operator+ taking a polygon-type and a numeric-type.
However, the underlying implementation requires a few more parameters, e.g. mitering, precisions etc.

I want to combine the power, expressiveness and terseness of the math notation with the control and precision of the underlying implementation. I'd like to write:

// somehow materialize a properly parametrized operator+=()
poly += 10;

and have this do just what I want.

There are a few options for injecting these parameters into the binary overloaded operator, but each has its own drawbacks:

  1. Set and use class-static/global (or thread local) parameters.
    But, using globals is bad for threading, discoverability, readability and cleanup.
  2. Keep the extra parameters as members of the operator input types.
    This puts the type wrapping burden on the caller and may make the code more cumbersome. This is also unclear in terms of consistency which of the input types should have these params and what happens in case of conflicting or multiple param values from both.
    Also, if I want to perform the operation multiple times, I have to respecify the parameters over and over.

Can anyone suggest another approach that may work here?

One option that I am considering but am not yet sure how to make work is to have the overloaded operator as a template function in a separate namespace. The template would be parametrized over the required params (assume they are known at compile time), and once instantiated the operator is somehow pulled into the currently discoverable namespace where the ADL will correctly match it. I am not sure if/how this can be pulled off.

Alternatively, use the same namespace hiding and revealing trick to add automatic type conversions for the operator input types so that they are automatically wrapped with types that provide the extra parameters.

Can this type of namespace hiding and exposing + ADL be done?

I guess this would be sorta like giving the operator a lambda-like behavior.

Upvotes: 2

Views: 115

Answers (2)

I warn in advance that my solution is a bit hackish and uscaleable. But if the sole purpose is to reduce the verbosity of the expression for the user.. You can do something akin to the following:

template<class Space>
class numeric
{
  long double _val;
public:
  numeric (long double val) : _val(val) {}
  operator long double() { return _val; }
};

struct s_mither;
numeric<s_mither> operator ""_mtr(long double v) { return v; }

enum class end_token_t : char {};
constexpr end_token_t end_token{};

class entity
{
  struct op_params_t
  {
    long double _scalar;
    long double _mither;
    bool        _in_op;

  } op_params;

public:
  entity& operator+= (long double scalar)
  { op_params._in_op = true; op_params._scalar = scalar; return *this;}

  entity& operator+= (numeric<s_mither> mither)
  { op_params._in_op = true; op_params._mither = mither; return *this;}

  entity& operator, (long double scalar)
  { if(op_params._in_op) op_params._scalar = scalar; return *this;}

  entity& operator, (numeric<s_mither> mither)
  { if(op_params._in_op) op_params._mither = mither; return *this;}

  void    operator, (end_token_t)
  { op_params._in_op = false; }
};


int main() {
    entity e;

    e += 10.0, 1.0_mtr, end_token;
    e += 20._mtr, 5, end_token;

    return 0;
}

It's ugly, but has the benefit of having all parameters be optional. Sadly I wasn't able to bypass the token.

A better solution will probably be to provide a start token which return a temporary "collector" object on the first comma. That way you can keep all the comma operator overloading outside of your actual entity class.

Upvotes: 1

John Zwinck
John Zwinck

Reputation: 249153

I would aim to support something like this:

poly += miter(10).limit(3)

That is:

class modifier
{
public:
    virtual polygon& add(polygon&) const = 0;
};

class miter : public modifier
{
public:
    miter(int width);
    miter& limit(int ml);

    virtual polygon& add(polygon&) const override;
};

polygon& operator +=(polygon& poly, const modifier& mod)
{
    return mod.add(poly);
}

If you want to apply the same operation to several polygons, you can do this:

miter mod(10);
mod.limit(3);
polyA += mod;
polyB += mod;
polyC += mod;

Upvotes: 2

Related Questions