C++/CLI: how to overload an operator to accept ref

2019-09-08 07:46发布

问题:

I am trying to create a CLI value class c_Location with overloaded operators, but I think I have an issue with boxing. I have implemented the operator overloading as seen in many manuals, so I'm sure this must be right. This is my code:

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'
}

After searching for a longer time, I found that the error comes from the operand being a reference type, as it is an array element of a value type, and the function accepting only value types as it takes an unmanaged reference. I now have 2 possibiblities:

  1. adding a unboxing cast to c_Location and so changing the faulty line in main() to
    locValue = alocData[0] + (c_Location)alocData[1];
  2. modifying the operator+ overloading so that it takes the parameter by value instead of by reference:
    c_Location operator+ (const c_Location i_locValue)

both options work, but as far as I can see, they both have disadvantages:
opt 1 means that I have to explicitly cast wherever needed.
opt 2 means that the function will create a copy of the parameter on its call and therefore waste performance (not much though).

My questions: Is my failure analysis correct at all or does the failure have another reason?
Is there a better third alternative?
If not: which option, 1 or 2, is the better one? I currently prefer #2.

回答1:

TL;DR version:

For managed code, use % for a pass by reference parameter, not &


You diagnosis is not completely correct. Boxing has nothing to do with your problem. But reference types do, in a way.

You were really close when you said that "I found that the error comes from the operand being a reference type". Well, the operand is a value type not a reference type. But the error occurs when the operand is stored inside a reference type, because then it's inside the garbage-collected heap (where all instances of reference types are placed). This goes for arrays as well as your own objects which contain a member of value type.

The danger is that when the garbage collector runs, it can move items around on the gc heap. And this breaks native pointers (*) and references (&), because they store the address and expect it to stay the same forever. To handle this problem, C++/CLI provides tracking pointers (^) and tracking references (%) which work together with the garbage collector to do two things:

  • make sure the enclosing object isn't freed while you're using it
  • find the new address if the garbage collector moves the enclosing object

For use from C++/CLI, you can make operator+ a non-member, just like normal 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;
}

The drawback is that C# won't use non-members, for compatibility with C#, write it like a non-member operator (with two explicit operands) but make it a public static member.

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

There's no reason to worry about this for operator+= since C# doesn't recognize that anyway, it will use operator+ and assign the result back to the original object.


For primitive types like double or int, you may find that you need to use % also, but only if you need a reference to an instance of that primitive type is stored inside a managed object:

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


回答2:

The rules are rather different from native C++:

  • the CLI demands that operator overloads are static members of the class
  • you can use the const keyword in C++/CLI but you get no mileage from it, the CLI does not support enforcing const-ness and there are next to no other .NET languages that support it either.
  • passing values of a value type ought to be done by value, that's the point of having value types in .NET in the first place. Using a & reference is very troublesome, that's a native pointer at runtime which the garbage collector cannot adjust. You'll get a compile error if you try to use your operator overload on a c_Location that's embedded in a managed class. If you want to avoid value copy semantics then you should declare a ref class instead. The hat^ in your code.
  • any interop type you create in C++/CLI should be declared public so it is usable from other assemblies and .NET languages. It isn't entirely clear if that's your intention, it is normally the reason you write C++/CLI code.

You could make your value class look like this instead:

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

Untested, ought to be close. You'll now see that your code in main() compiles without trouble.