I am getting a compiler error that I can't well explain or even
understand the origin of (though I boiled it down close...). Below is
a bare-bones example.
What I am doing is defining the increment operator in a class and then
defining a derived class and using the derived class. When I try to
use the increment operator on an instance of the derived class (as an
instance of the derived class, not when used as an instance of the base
class), I get a compiler error (detailed below). While I realize that
there are a number of options I can use to get around this, the problem
itself has become a point of interest.
This code causes the compiler error:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
namespace QuickTest {
public class Bar : ReadOnlyCollection<string{
protected int _CurrentPosition; //The currently active
value.
protected Bar _NextMostSignificantBar; //The next most
significant value (as in 2 is the next significant digit from the
perspective of 7 in the number 27).
public Bar( Bar bar )
: base( new List<string>() ) {
_CurrentPosition = 0;
_NextMostSignificantBar = bar;
}
public static Bar operator ++( Bar bar ) {
if ( ( bar._CurrentPosition + 1 ) == bar.Count ) {
if ( null != bar._NextMostSignificantBar ) {
bar._NextMostSignificantBar++;
}
bar._CurrentPosition = 0;
}
else { bar._CurrentPosition++; }
return bar;
}
public static implicit operator string( Bar bar ) {
return bar[bar._CurrentPosition];
}
}
public class NumericBar : Bar {
public NumericBar( Bar bar )
: base( bar ) {
this.Items.Add( "0" );
this.Items.Add( "1" );
this.Items.Add( "2" );
this.Items.Add( "3" );
this.Items.Add( "4" );
this.Items.Add( "5" );
this.Items.Add( "6" );
this.Items.Add( "7" );
this.Items.Add( "8" );
this.Items.Add( "9" );
}
}
public class Program {
public static void Main( string[] args ) {
NumericBar tens = new NumericBar( null );
NumericBar ones = new NumericBar( tens );
for ( int i = 0; i < tens.Count * ones.Count + 2; i++ ) {
Console.WriteLine( tens + ones );
// Causes compiler error: error CS0266: Cannot
implicitly
// convert type 'QuickTest.Bar' to
'QuickTest.NumericBar'.
// An explicit conversion exists (are you missing a
cast?)
ones++;
}
Console.WriteLine( "Press any key to exit." );
Console.ReadKey();
}
}
}
On the other hand, if I change the increment operator definition to
read (define it as a function, rather than an operator):
public static Bar op_Increment( Bar bar ) {
if ( ( bar._CurrentPosition + 1 ) == bar.Count ) {
if ( null != bar._NextMostSignificantBar ) {
Bar.op_Increment( bar._NextMostSignificantBar );
}
bar._CurrentPosition = 0;
}
else { bar._CurrentPosition++; }
return bar;
}
And the consuming code to:
Bar.op_Increment( ones );
I don't have any problems, but it is kind of nasty and really defeats
the whole idea (Besides, at this point, I was wondering what was going
wrong).
If I leave the increment operator definition as is and change the
consuming code to read:
((Bar)ones)++;
I get the following run-time exception:
System.InvalidProgramException was unhandled
Message="Common Language Runtime detected an invalid program."
Source="QuickTest"
StackTrace:
at QuickTest.Program.Main(String[] args)
at System.AppDomain.nExecuteAssembly(Assembly assembly, String[]
args)
at System.AppDomain.ExecuteAssembly(String assemblyFile,
Evidence assemblySecurity, String[] args)
at
Microsoft.VisualStudio.HostingProcess.HostProc.Run UsersAssembly()
at System.Threading.ThreadHelper.ThreadStart_Context( Object
state)
at System.Threading.ExecutionContext.Run(ExecutionCon text
executionContext, ContextCallback callback, Object state)
at System.Threading.ThreadHelper.ThreadStart()
Which doesn't make a whole lot of sense at first, but I believe that
the attempted cast is causing the NumericBar( Bar bar ) constructor to
be used (when it attempts to cast the object from the Bar class to a
NumericBar object) and thereby calling the constructor without using
new and passing it the object that is to be cast as it's
"parent"...
The problem can be circumvented if I change the line:
NumericBar ones = new NumericBar( tens );
To read:
Bar ones = new NumericBar( tens );
But that is a confusing requirement to place on an end developer and
I'm not sure how or where we would document it.
Since the conceptual paradigm of the code seems not to make sense, the
next step I took was to look into the IL code that expresses the
operator definition. It turns out that the only difference is the
translation of the "recursive" call on the "next most significant
Bar" (when rolling around) within the operator/function definition.
Commenting out that line of code (the "recursive" call) does not
make the compiler error go away, so here is the IL code that is
produced:
//000022: Bar.op_Increment(
bar._NextMostSignificantBar );
IL_0027: /* 02 | */ ldarg.0
IL_0028: /* 7B | (04)000002 */ ldfld class
QuickTest.Bar/*02000002*/
QuickTest.Bar/*02000002*/::_NextMostSignificantBar /* 04000002 */
IL_002d: /* 28 | (06)000002 */ call class
QuickTest.Bar/*02000002*/ QuickTest.Bar/*02000002*/::op_Increment(class
QuickTest.Bar/*02000002*/) /* 06000002 */
IL_0032: /* 26 | */ pop
.line 23,23 : 17,18 ''
Rather than:
//000020: bar._NextMostSignificantBar++;
IL_0027: /* 02 | */ ldarg.0
IL_0028: /* 25 | */ dup
IL_0029: /* 7B | (04)000002 */ ldfld class
QuickTest.Bar/*02000002*/
QuickTest.Bar/*02000002*/::_NextMostSignificantBar /* 04000002 */
IL_002e: /* 28 | (06)000002 */ call class
QuickTest.Bar/*02000002*/ QuickTest.Bar/*02000002*/::op_Increment(class
QuickTest.Bar/*02000002*/) /* 06000002 */
IL_0033: /* 7D | (04)000002 */ stfld class
QuickTest.Bar/*02000002*/
QuickTest.Bar/*02000002*/::_NextMostSignificantBar /* 04000002 */
.line 21,21 : 17,18 ''
The main difference is the dup, ldfld, call, and then use of stfld
rather than a ldfld, call, and then pop in the sequence of calls that
represents the . Those four calls are documented as (a fifth is listed
and will be implicitly referred to later):
Nerfed Table of the opcodes:
Format
Assembly Format
Description
7D < T >
stfld field
Replaces the value of field of the object with a new value.
7B < T >
ldfld field
Pushes the value of a field in a specified object onto the stack.
28 < T >
call methodDesc
Call the method described by methodDesc.
25
dup
Duplicates the value on the top of the stack.
FE 0E < unsigned int16 >
stloc index
Pops a value from the stack and stores it in local variable index.
So... the increment operator case duplicates the
_NextMostSignificantBar field, then executes the increment call on it
and tries to reassign the duplicated _NextMostSignificantBar to the
object's _NextMostSignificantBar.
Then I found that trying to comment out the "recursive" increment
operation wasn't targeting the error, since the error is occurring in
the code in Main(str[]), so I moved on to looking at that code (using
the NumericBar =Bar definition hack to produce the code). Here is
the IL:
//000060: ones++;
IL_002d: /* 07 | */ ldloc.1
IL_002e: /* 28 | (06)000002 */ call class
QuickTest.Bar/*02000002*/ QuickTest.Bar/*02000002*/::op_Increment(class
QuickTest.Bar/*02000002*/) /* 06000002 */
IL_0033: /* 0B | */ stloc.1
And in the case of the function call:
//000061: Bar.op_Increment( ones );
IL_002d: /* 07 | */ ldloc.1
IL_002e: /* 28 | (06)000002 */ call class
QuickTest.Bar/*02000002*/ QuickTest.Bar/*02000002*/::op_Increment(class
QuickTest.Bar/*02000002*/) /* 06000002 */
IL_0033: /* 26 | */ pop
Of course, the difference is the use of stloc.1 (which copies the value
on the stack to the "ones" object [which is itself, but had been
implicitly cast to a Bar object]) rather than pop, which is sufficient
since the changed data is referenced within the "ones" object. The
stloc's copy action seems (I'm guessing) to be type-safe (or maybe
the compiler/VS just analyzes it as a type cast) and is thereby the
cause of the compiler error. Remember that the increment operator
takes a Bar object and returns a Bar object (the same object passed in)
and that the increment is being called on a NumericBar object.
With the problem identified, I am at a point where all I can do is ask
a question: what is the correct way to define an increment operator
that will be usable on the derived classes of the defining superclass?
Is this even possible? It seems sensible. Is this a bug (or simply a
unexpected use) in the operator/class structure relationship?
I don't need a solution really, what I have done is to define a
non-static virtual method "Increment" that is called from the short
definition of the ++ operator that I have defined on all the derived
classes:
public static Bar operator ++( Bar bar ) {
bar.Increment();
return bar;
}
I did find a discussion of the inheritance of overloaded operators at:
http://www.codeproject.com/csharp/cssimpolymorph.asp where Jeffery Sax
explained that inheritance of operator overloading can cause ambiguity,
but that was based on ordering effects with binary operators and in the
case of a unary operator (which must be defined statically targeting
the specific defining type), ordering is not an issue.