【问题标题】:Parametrized Operator Overloading参数化运算符重载
【发布时间】:2016-04-27 08:33:37
【问题描述】:

我的库有几个自然映射到算术和逻辑/布尔运算符的操作。但是,这些操作需要除了lhsrhs 之外的其他参数。

这是我用ClipperLib 演示的几何示例:
假设我想创建一个多边形偏移运算符,它采用 2D 多边形和偏移增量并执行多边形偏移。这个例子来自 ClipperLib:
从数学上讲,此操作可以映射到采用多边形类型和数字类型的operator+
然而,底层的实现需要更多的参数,例如斜接、精度等。

我想将数学符号的强大、表达力和简洁性与底层实现的控制力和精度结合起来。我想写:

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

让这个做我想做的事。

有几个选项可以将这些参数注入二元重载运算符,但每个选项都有自己的缺点:

  1. 设置和使用类静态/全局(或线程本地)参数。
    但是,使用全局变量不利于线程、可发现性、可读性和清理。
  2. 将额外参数保留为运算符输入类型的成员。
    这给调用者增加了类型包装的负担,并且可能使代码更加繁琐。就一致性而言,这也不清楚哪些输入类型应该具有这些参数,以及在两者发生冲突或多个参数值的情况下会发生什么。
    另外,如果我想多次执行该操作,我必须一遍又一遍地重新指定参数。

任何人都可以提出另一种可能适用于此的方法吗?

我正在考虑但不确定如何进行的一个选项是将重载的运算符作为模板函数放在单独的命名空间中。模板将在所需参数上进行参数化(假设它们在编译时已知),并且一旦实例化,操作符就会以某种方式被拉入当前可发现的命名空间,ADL 将在其中正确匹配它。 我不确定这是否/如何实现。

或者,使用相同的命名空间隐藏和显示技巧为运算符输入类型添加自动类型转换,以便它们自动包装为提供额外参数的类型。

这种命名空间隐藏和暴露+ADL可以做到吗?

我想这有点像给操作员一个类似 lambda 的行为。

【问题讨论】:

  • poly += CArgs(10).decorateWithPrecision(0.15).decorateWithLineWidth(1.5); 怎么样?
  • CArgs?您的意思是像选项 2 一样创建自定义包装器?虽然这种语法还不错,但我想知道是否可以以某种方式将额外的代码移入运算符实例化中。
  • 是选项 2。您有 2 个选项。如果 args 是常量,您可以使用 CArg 作为模板类。否则,您可以向构造函数添加更多参数,但这意味着装饰不是可选的。我会编写简单版本的代码并让优化器模块完成工作。
  • 您可以尝试类似 std:: 的流类为您提供的使用操纵器更改其运算符的东西(例如 cout << hex << MyVar,以十六进制而不是十进制打印 MyVar)。我不确定我会为+ 操纵器选择哪种语法,但可能值得考虑。

标签: c++ operator-overloading template-meta-programming higher-order-functions argument-dependent-lookup


【解决方案1】:

我的目标是支持这样的事情:

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

即:

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);
}

如果你想对多个多边形应用相同的操作,你可以这样做:

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

【讨论】:

  • 是的,这就是我对选项 2 的想法。如果我想多次执行该操作,例如在多个多边形上,我必须一遍又一遍地重新指定参数。
  • @AdiShavit:如果你想多次执行相同的操作,你可以!我在答案的底部添加了一个 sn-p,显示了如何。
  • 当然。我的意思是:polyA += 10; polyB += 3;...,这意味着polyA += miter(10).limit(3); polyB += miter(3).limit(3); ...。后者显然更冗长。如果+= 是一个类似labmda 的对象并且限制是捕获,那么第一个sn-p 就足够了。
  • @AdiShavit:现在听起来你想要全局变量。如果您愿意,您当然可以使用它!
  • 所以你基本上想要auto mod = std::bind(miter, _1, 3); mod(polyA, 10); mod(polyB, 3); 但使用中缀表示法而不是常规函数调用表示法?我不明白这是怎么可能的。
【解决方案2】:

我提前警告说,我的解决方案有点骇人听闻且无法扩展。但是,如果唯一的目的是为用户减少表达式的冗长性。你可以做类似于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;
}

这很丑,但好处是所有参数都是可选的。 遗憾的是我无法绕过令牌。

一个更好的解决方案可能是提供一个开始标记,它在第一个逗号上返回一个临时的“收集器”对象。这样你就可以在你的实际实体类之外保持所有的逗号运算符重载。

【讨论】:

  • +1 表示原创性,但我不能说这会提高可读性,这才是这里的重点:-)。即使没有end_token,我相信语法和语义都不是很清楚。用户需要记住按什么顺序发生的事情,我认为这负担太大了。
  • @AdiShavit,我同意。这就是为什么我喜欢使用自定义文字。我想知道用户添加命名运算符的概念是否更像是朝着正确方向迈出的一步。 github.com/klmr/named-operator
  • @StoryTelller:考虑过,但就我而言,算术和布尔运算符正是我所需要的(并且它们的优先级已经保证是正确的)。但是,是的,使用自定义运算符技巧,可以添加额外的“预参数”。
猜你喜欢
  • 2015-08-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多