Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Equivalent implicit operators: why are they legal?

Update!

See my dissection of a portion of the C# spec below; I think I must be missing something, because to me it looks like the behavior I'm describing in this question actually violates the spec.

Update 2!

OK, upon further reflection, and based on some comments, I think I now understand what's going on. The words "source type" in the spec refer to the type being converted from -- i.e., Type2 in my example below -- which simply means that the compiler is able to narrow the candidates down to the two operators defined (since Type2 is the source type for both). However, it cannot narrow the choices any further. So the key words in the spec (as it applies to this question) are "source type", which I previously misinterpreted (I think) to mean "declaring type."


Original Question

Say I have these types defined:

class Type0
{
    public string Value { get; private set; }

    public Type0(string value)
    {
        Value = value;
    }
}

class Type1 : Type0
{
    public Type1(string value) : base(value) { }

    public static implicit operator Type1(Type2 other)
    {
        return new Type1("Converted using Type1's operator.");
    }
}

class Type2 : Type0
{
    public Type2(string value) : base(value) { }

    public static implicit operator Type1(Type2 other)
    {
        return new Type1("Converted using Type2's operator.");
    }
}

Then say I do this:

Type2 t2 = new Type2("B");
Type1 t1 = t2;

Obviously this is ambiguous, as it is not clear which implicit operator should be used. My question is -- since I cannot see any way to resolve this ambiguity (it isn't like I can perform some explicit cast to clarify which version I want), and yet the class definitions above do compile -- why would the compiler allow those matching implicit operators at all?


Dissection

OK, I'm going to step through the excerpt of the C# spec quoted by Hans Passant in an attempt to make sense of this.

Find the set of types, D, from which user-defined conversion operators will be considered. This set consists of S (if S is a class or struct), the base classes of S (if S is a class), and T (if T is a class or struct).

We're converting from Type2 (S) to Type1 (T). So it seems that here D would include all three types in the example: Type0 (because it is a base class of S), Type1 (T) and Type2 (S).

Find the set of applicable user-defined conversion operators, U. This set consists of the user-defined implicit conversion operators declared by the classes or structs in D that convert from a type encompassing S to a type encompassed by T. If U is empty, the conversion is undefined and a compile-time error occurs.

All right, we've got two operators satisfying these conditions. The version declared in Type1 meets the requirements because Type1 is in D and it converts from Type2 (which obviously encompasses S) to Type1 (which is obviously encompassed by T). The version in Type2 also meets the requirements for exactly the same reasons. So U includes both of these operators.

Lastly, with respect to finding the most specific "source type" SX of the operators in U:

If any of the operators in U convert from S, then SX is S.

Now, both operators in U convert from S -- so this tells me that SX is S.

Doesn't this mean that the Type2 version should be used?

But wait! I'm confused!

Couldn't I have only defined Type1's version of the operator, in which case, the only remaining candidate would be Type1's version, and yet according to the spec SX would be Type2? This seems like a possible scenario in which the spec mandates something impossible (namely, that the conversion declared in Type2 should be used when in fact it does not exist).

like image 289
Dan Tao Avatar asked Aug 24 '10 22:08

Dan Tao


People also ask

What happens in implicit conversion?

An implicit conversion sequence is the sequence of conversions required to convert an argument in a function call to the type of the corresponding parameter in a function declaration. The compiler tries to determine an implicit conversion sequence for each argument.

What is an implicit operator in c#?

The Implicit Operator According to MSDN, an implicit keyword is used to declare an implicit user-defined type conversion operator. In other words, this gives the power to your C# class, which can accepts any reasonably convertible data type without type casting.

Which operator can be used to do type conversion in c#?

Use the operator and implicit or explicit keywords to define an implicit or explicit conversion, respectively. The type that defines a conversion must be either a source type or a target type of that conversion. A conversion between two user-defined types can be defined in either of the two types.

What is implicit conversion c++?

Implicit type conversion also known as automatic type conversion is carried out by the compiler without the need for a user-initiated action. It takes place when an expression of more than one data type is present which in such an instance type conversion takes place to avoid data loss.


2 Answers

Ultimately, it can't be prohibitted with complete success. You and I could publish two assemblies. They we could start using each other's assembles, while updating our own. Then we could each provide implicit casts between types defined in each assembly. Only when we release the next version, could this be caught, rather than at compile time.

There's an advantage in not trying to ban things that can't be banned, as it makes for clarity and consistency (and there's a lesson for legislators in that).

like image 132
Jon Hanna Avatar answered Oct 01 '22 13:10

Jon Hanna


We don't really want it to be a compile-time error just to define conversions which might cause ambiguity. Suppose that we alter Type0 to store a double, and for some reason we want to provide separate conversions to signed integer and unsigned integer.

class Type0
{
    public double Value { get; private set; }

    public Type0(double value)
    {
        Value = value;
    }

    public static implicit operator Int32(Type0 other)
    {
        return (Int32)other.Value;
    }

    public static implicit operator UInt32(Type0 other)
    {
        return (UInt32)Math.Abs(other.Value);
    }

}

This compiles fine, and I can use use both conversions with

Type0 t = new Type0(0.9);
int i = t;
UInt32 u = t;

However, it's a compile error to try float f = t because either of the implicit conversions could be used to get to an integer type which can then be converted to float.

We only want the compiler to complain about these more complex ambiguities when they're actually used, since we'd like the Type0 above to compile. For consistency, the simpler ambiguity should also cause an error at the point you use it rather than when you define it.

EDIT

Since Hans removed his answer which quoted the spec, here's a quick run through the part of the C# spec that determines whether a conversion is ambiguous, having defined U to be the set of all the conversions which could possibly do the job:

  • Find the most specific source type, SX, of the operators in U:
    • If any of the operators in U convert from S, then SX is S.
    • Otherwise, SX is the most encompassed type in the combined set of target types of the operators in U. If no most encompassed type can be found, then the conversion is ambiguous and a compile-time error occurs.

Paraphrased, we prefer a conversion which converts directly from S, otherwise we prefer the type which is "easiest" to convert S to. In both examples, we have two conversions from S available. If there were no conversions from Type2, we'd prefer a conversion from Type0 over one from object. If no one type is obviously the better choice to convert from, we fail here.

  • Find the most specific target type, TX, of the operators in U:
    • If any of the operators in U convert to T, then TX is T.
    • Otherwise, TX is the most encompassing type in the combined set of target types of the operators in U. If no most encompassing type can be found, then the conversion is ambiguous and a compile-time error occurs.

Again, we'd prefer to convert directly to T, but we'll settle for the type that's "easiest" to convert to T. In Dan's example, we have two conversions to T available. In my example, the possible targets are Int32 and UInt32, and neither is a better match than the other, so this is where the conversion fails. The compiler has no way to know whether float f = t means float f = (float)(Int32)t or float f = (float)(UInt32)t.

  • If U contains exactly one user-defined conversion operator that converts from SX to TX, then this is the most specific conversion operator. If no such operator exists, or if more than one such operator exists, then the conversion is ambiguous and a compile-time error occurs.

In Dan's example, we fail here because we have two conversions left from SX to TX. We could have no conversions from SX to TX if we chose different conversions when deciding SX and TX. For example, if we had a Type1a derived from Type1, then we might have conversions from Type2 to Type1a and from Type0 to Type1 These would still give us SX=Type2 and TX=Type1, but we don't actually have any conversion from Type2 to Type1. This is OK, because this really is ambiguous. The compiler doesn't know whether to convert Type2 to Type1a and then cast to Type1, or cast to Type0 first so that it can use that conversion to Type1.

like image 26
stevemegson Avatar answered Oct 01 '22 13:10

stevemegson