2

我正在尝试使用重载运算符创建 CLI 值类 c_Location,但我认为我遇到了装箱问题。我已经实现了许多手册中所见的运算符重载,所以我确信这一定是正确的。这是我的代码:

value class c_Location
{
public:
  double x, y, z;
  c_Location (double i_x, double i_y, double i_z) : x(i_x), y(i_y), z(i_z) {}

  c_Location& operator+= (const c_Location& i_locValue)
  {
    x += i_locValue.x;
    y += i_locValue.y;
    z += i_locValue.z;
    return *this;
  }
  c_Location operator+ (const c_Location& i_locValue)
  {
    c_Location locValue(x, y, z);
    return locValue += i_locValue;
  }
};

int main()
{
  array<c_Location,1>^ alocData = gcnew array<c_Location,1>(2);
  c_Location locValue, locValue1, locValue2;
  locValue = locValue1 + locValue2;
  locValue = alocData[0] + alocData[1];  // Error C2679 Binary '+': no operator found which takes a right-hand operand of type 'c_Location'
}

找了很久,发现错误是因为操作数是引用类型,因为它是值类型的数组元素,而函数只接受值类型,因为它是非托管引用。我现在有两种可能性:

  1. 添加一个拆箱演员c_Location,因此将 main() 中的错误行更改为
    locValue = alocData[0] + (c_Location)alocData[1];
  2. 修改 operator+ 重载,使其按值而不是按引用获取参数:
    c_Location operator+ (const c_Location i_locValue)

这两个选项都有效,但据我所知,它们都有缺点:
选择 1 意味着我必须在需要的地方显式转换。
opt 2 意味着该函数将在其调用时创建参数的副本,因此会浪费性能(虽然不多)。

我的问题:我的故障分析完全正确还是故障有其他原因?
有没有更好的第三种选择?
如果不是:1 或 2 哪个选项更好?我目前更喜欢#2。

4

2 回答 2

3

规则与原生 C++ 有很大不同:

  • CLI 要求运算符重载是类的静态成员
  • 您可以在 C++/CLI 中使用const关键字,但不会从中受益,CLI 不支持强制执行 const-ness,并且几乎没有其他 .NET 语言支持它。
  • 传递值类型的值应该按值完成,这首先是在 .NET 中具有值类型的要点。使用 & 引用非常麻烦,这是运行时的原生指针,垃圾收集器无法调整。如果您尝试在托管类中嵌入的 c_Location 上使用运算符重载,则会出现编译错误。如果你想避免值复制语义,那么你应该声明 a ref class。代码中的帽子^。
  • 您在 C++/CLI 中创建的任何互操作类型都应声明为公共的,以便可以从其他程序集和 .NET 语言中使用。这是否是您的意图并不完全清楚,这通常是您编写 C++/CLI 代码的原因。

你可以让你的价值类看起来像这样:

public value class c_Location
{
public:
  double x, y, z;
  c_Location (double i_x, double i_y, double i_z) : x(i_x), y(i_y), z(i_z) {}

  static c_Location operator+= (c_Location me, c_Location rhs)
  {
    me.x += rhs.x;
    me.y += rhs.y;
    me.z += rhs.z;
    return me;
  }
  static c_Location operator+ (c_Location me, c_Location rhs)
  {
    return c_Location(me.x + rhs.x, me.y + rhs.y, me.z + rhs.z);
  }
};

未经测试,应该接近。您现在将看到您在 main() 中的代码可以顺利编译。

于 2013-08-02T15:04:07.627 回答
2

TL;DR 版本:

对于托管代码,%用于通过引用传递参数,而不是&


你的诊断并不完全正确。拳击与你的问题无关。但在某种程度上,引用类型确实如此。

当您说“我发现错误来自操作数是引用类型”时,您真的很接近。好吧,操作数是值类型而不是引用类型。但是当操作数存储引用类型中时会发生错误,因为它位于垃圾收集堆中(放置所有引用类型的实例)。这适用于数组以及您自己的包含值类型成员的对象。

危险在于当垃圾收集器运行时,它可以在 gc 堆上移动项目。这会破坏本地指针 ( *) 和引用 ( &),因为它们存储地址并希望它永远保持不变。为了解决这个问题,C++/CLI 提供了跟踪指针(^)和跟踪引用(%),它们与垃圾收集器一起做两件事:

  • 确保在使用时未释放封闭对象
  • 如果垃圾收集器移动封闭对象,则找到新地址

对于从 C++/CLI 使用,您可以创建operator+一个非成员,就像普通的 C++ 一样。

value class c_Location
{
public:
    double x, y, z;
    c_Location (double i_x, double i_y, double i_z) : x(i_x), y(i_y), z(i_z) {}

    c_Location% operator+= (const c_Location% i_locValue)
    {
        x += i_locValue.x;
        y += i_locValue.y;
        z += i_locValue.z;
        return *this;
    }
};

c_Location operator+ (c_Location left, const c_Location% right)
{
    return left += right;
}

缺点是 C# 不会使用非成员,为了与 C# 兼容,将其编写为非成员运算符(具有两个显式操作数),但使其成为公共静态成员。

value class c_Location
{
public:
    double x, y, z;
    c_Location (double i_x, double i_y, double i_z) : x(i_x), y(i_y), z(i_z) {}

    c_Location% operator+= (const c_Location% i_locValue)
    {
        x += i_locValue.x;
        y += i_locValue.y;
        z += i_locValue.z;
        return *this;
    }

    static c_Location operator+ (c_Location left, const c_Location% right)
    {
        return left += right;
    }
};

没有理由担心这一点,operator+=因为 C# 无论如何都无法识别,它会使用operator+并将结果分配回原始对象。


对于doubleor之类的原始类型int,您可能会发现也需要使用%,但前提是您需要对该原始类型的实例的引用存储在托管对象中:

double d;
array<double>^ a = gcnew darray<double>(5);
double& native_ref = d; // ok, d is stored on stack and cannot move
double& native_ref2 = a[0]; // error, a[0] is in the managed heap, you MUST coordinate with the garbage collector
double% tracking_ref = d; // ok, tracking references with with variables that don't move, too
double% tracking_ref2 = a[0]; // ok, now you and the garbage collector are working together
于 2013-08-02T14:04:26.977 回答