C# to IL 5 Operator Overloading(操作符重载)
Every operator overload that we use in C#, gets converted to a function call in IL. The
overloaded > operator translates into the function op_GreaterThan and a + gets converted
to op_Addition etc. In the first program of this chapter, we have overloaded the + operator
in class yyy to facilitate adding of two yyy objects.
While using the plus (+) operator on the two yyy objects, C# is aware that IL does not
support operator overloading. Therefore, it creates a function called op_Addition in the
class yyy.
Thus, operator overloading gets represented as a mere function call. The rest of the code is
easy for you to figure out.
In IL, there is no rule stating that if the > operator is overloaded, then the < operator also
has to be overloaded. These rules are imposed by the C# compiler, and not by IL since, IL
does not support the concept of overloading at all.
The C# compiler is extremely intelligent. Whenever a yyy object has to be converted to a
string, it first checks for the presence of an operator called string in the class yyy. If it
exists, it calls that operator.
The operator named string is a predefined data type in C#. Hence, it is converted into the
operator op_Implicit. This operator takes a yyy object as a parameter. It returns a string on
the stack for the WriteLine function. The ToString function is not called.
C# will generate an error if you alter even a single parameter to the operator string, but
such is not a case with IL as it does not support operator overloading and conversions
In the C# code above, we have dispensed with the operator string and instead, have used
the ToString function. As usual, we put the object a on the stack. In the IL code given
earlier, due to the presence of operator overloads in the C# code, the function op_Implicit
was called. In this case, since there are no operator overloads, the object reference to object
a is simply put on the stack. In class yyy, even though, the function ToString is not
explicitly called, the function does get executed.
Since the ToString is virtual in the class Object, at run time, the ToString function is called
from the class yyy, instead of being called from the class Object. This is due to the concept
of a vtable, where all virtual function addresses reside.
If the word virtual is removed from the function, the ToString function gets called from the
class Object instead of the class yyy
In the above code, we have cast a yyy object into a string using an explicit cast. IL does not
understand C# keywords like implicit or explicit. It converts the cast to an actual function
such as op_Explicit or op_Implicit. Thus writing a C# compiler requires a lot of grey matter.
In the code above, we are not creating an object that is an instance of class yyy. Instead,
we are simply initializing it to a numeric value of 10. This results in a call to the implicit
operator yyy, which takes an int value as a parameter and creates a yyy object.
The IL code does not understand any of this. It simply calls the relevant operator, which in
this case is op_Implicit, with an int value. It is the responsibility of this function to create
an object that is an instance of class yyy. We are, in effect, creating two locals that look like
yyy, and initializing them to the new yyy like object on the stack. Finally its value,10, is put
on the stack.
In the above code, we have created two objects, a and b, that are instances of a class yyy.
Then, we have employed the overloaded operators & and && to determine as to how IL
handles them internally. If we can grasp the intricacies of IL, our understanding of C# will
become so much better. Maybe, a programmer should be allowed to program in C# only if
he/she has learnt IL.
The dup operator duplicates the value present at the top of the stack. In this case, it is the
local V_0. All occurences of && and & in the C# code are replaced by the functions
op_False and op_BitwiseAnd respectively, on conversion to IL code.
The op_False operator returns either TRUE or FALSE.
• If it returns TRUE, then the answer is TRUE, and the rest of the condition is not
checked. This is how the code is short-circuited. We simply jump past code that is not
to be executed.
• If it returns FALSE, the & operator gets called. This operator gets converted to
op_BitwiseAnd. In order to enhance the efficiency, the two objects were already present
on the stack for the op_BitwiseAnd operator to act upon.
You will be appreciate that IL makes our understanding of abstract concepts of C# much
easier to understand.
In IL, the object m is a local named V_0 of type System.Type. In C#, the typeof keyword
returns a Type object, but in IL, a large number of steps have to be executed to achieve the
same result.
• Firstly, a type is placed on the stack using the instruction ldtoken. This loads a token
that represents a type or a field or a method.
• Next, the function GetTypeFromHandle is called that picks up a token, i.e. a
structure or value class from the stack.
• The function thereafter returns a Type object representing a type, which in this case
is an int. This is stored in the local V_0 and then again loaded on the stack.
• Next, the function get_FullName is called. The function is not called FullName but
get_FullName as it is a property. This property returns a string on the stack that is
displayed using the WriteLine function.
The keyword is lets us determine the data type of an object at run-time. Thus the is
keyword of C# has an equivalent instruction in IL.We are passing a zzz like object and an
object that is an instance of class object to the function abc. This function demotes every
parameter it receives to a class object, but the is keyword is intelligent enough to know
that the run time data type can be of a type other than an object. Thus, it returns TRUE for
the z object, but not for the a object.
The assembler code in Main or vijay remains the same. The relevant source code is
present in the function abc.
• The instruction ldarg.1 pushes the value of parameter 1 onto the stack. The data type
of this parameter is Object.
• Next, the instruction isinst is called. The type with which we want to compare the
object on the stack is passed as a parameter to isinst. This instruction determines the
data type of the value present on the stack.
• If the type of the isint instruction matches what is already there on the stack, the
object remains on the stack. If it does not match, a NULL is placed on the stack.
• The brfalse instruction executes the jump to a label if the result is TRUE in the il
code.
The keyword as is similar to the is. Two objects have been placed on the stack and the
function abc is called. This function requires an object on the stack. The type of the
variable a has to be converted from int to an Object. The isinst instruction takes value at
the top of the stack and converts it into the data type specified. If it is unable to do so, it
puts a NULL on the stack.
In the second call, on the stack, a string is obtained for the WriteLine function. Since an
int32 value cannot be converted into a string, a NULL value is placed on the stack. Hence
the WriteLine function displays a blank line.
All pointers in C# have a size of 4 bytes each. The sizeof keyword is an instruction in IL
that returns the size of the variable that is passed as a parameter to it. It can only be used
on a value type variable, not on a reference type.
In C# we use the modifier unsafe while introducing pointers. This modifier does not exist
in IL, as IL regards everything as unsafe. Note that a byte in C# is converted into an int8 in
IL.
In the following program, the main function calls a function called abc. That part of the
code has already been explained previously. The remaining part of the code is explained in
the next few lines in bullet form.
In C#, whenever we want to obtain the address of a variable, we have to precede the name
of the variable with the symbol &. The & places the address of a variable on the stack. IL
interprets a pointer as a data type.
• We start by creating a pointer to an int i in C#. V_0 is interpreted as a pointer due to
the * sign that precedes it.
• Next, we initialize the variable j or V_1 to the value 1.
• The instruction ldloca.s places the address of j or V_1 on the stack.
• The instruction stloc.0 initializes V_0 to this value i.e. the address of j or V_1.
• The instruction ldloc.0 then places the value of the pointer on the stack and calls the
WriteLine function with an int as a parameter.
• We then place the value of the pointer that is pointing to int j in memory, on the
stack.
• Next, we place the number 10 on the stack.
• The instruction stind places the current value on the stack i.e. 10 into the memory
location placed earlier on the stack. Thus, we have utilised stind to fill up a certain
memory location with a specific value. This value is the address of the variable j in
memory.
• The WriteLine function is finally called to display the new value of the variable j.
The above program is presented to demonstrate that the C# compiler understands pointer
arithmetic, whereas IL does not.
The crucial line in the above code is the one that contains the code ldc.i4.4. The C#
compiler calculates that a pointer to an int has a size of 4 and therefore, it puts this
instruction in the IL code to facilitate pointer arithmetic.
Had we replaced the int by a short, the C# compiler would have replaced the ldc
instruction with the code ldc.i4.2 because, it is aware that the size of a pointer to short is
2. Thus, we can safely conclude that it is the C# compiler that understands pointer
arithmetics and not IL.
In C#, the stackalloc function allocates a certain amount of memory on the stack whereas,
new allocates memory on the heap. Heap memory is longer lasting than stack memory.
The equivalent of this function in the IL instruction set is localloc. The parameter to this
function specifies the amount of memory to be allocated. In the C# program, we have
specified that we want to allocate memory for 100 ints. Since each int requires 4 bytes of
memory, in IL, the numbers 4 and 100 are put on the stack and they are multiplied using
the mul operator. Thus, a total of 400 bytes of memory are finally allocated.
The assembler does not check the signature for the entrypoint function. But at run-time,
the signature is checked to confirm whether it has only one parameter or not. Since there
are two parameters in the entrypoint function, the run time exception has been generated.
If there had been a single int parameter, no exception would have occurred at run-time.
The directive entrypoint cannot be present in more that one function, even if they are in
separate classes. This is already illustrated in Chapter 1.
An enum is implemented as a class that is serializable. This means that the CLR can write
it to a disk or send it over a network. It extends the class System.Enum.
In the C# program, three enums are created. On conversion to IL, three corresponding
literal fields with the same names are created. The values of the enum variables are
calculated at compile time. There is a special variable introduced called value__.
Also, in the function vijay, the value of enum 'black' is being displayed. Observe carefully,
there is no mention of 'black' in the generated IL code.
IL handles this situation in the following chronological steps:
• First, it puts the number 1 on the stack.
• Then, it stores this value 1 in the yyy value class or structure V_0.
• Next, it uses ldloca.s to place the address of the variable V_0 on the stack.
• Thereafter, it uses box to convert it into an object.
• Finally, the value 1 is stored in the value class yyy using instruction stloc.0.
Thus, it may be appreciated that IL discards all the enum names and only deals with the
values. However, we cannot get rid of the special variable value__ because its omission will
result in an error at run time.
It can be seen from the code above that in the IL file, the expression 10 + aa.a2 is
conspicuous by its absence. On generation of the IL code, the expression gets converted to
its actual value i.e. 11.
After examining the above code, we can be rest assured that enums, like other artefacts
mentioned earlier, exist only in the realm of C# and have no direct representation in IL
When we try to compare an enum with a number using the comparison operator ==, this
operator gets replaced with the value FALSE at run time. Therefore, the IL code that is
generated is vastly at variance with the original C# code
The switch statement of C# is converted to the switch instruction in IL. This instruction
checks the value at the top of the stack and accordingly branches to the relevant label.
• If the value is 0, it branches to the label IL_0012.
• If the value is 1, it branches to the label IL_001e and so on.
If none of the cases match, the default clause will apply. In this case, the br.s IL_002a
instruction is executed
In the previous example, we consciously used consecutive values such as 0, 1 and so on.
In this example, we have used discontinuous values like 0 and 5.
On conversion to IL code, we do not see the instruction switch, but instead, we see a series
of jumps. The instruction beq.s is based on ceq and brtrue.s.
We place the individual case values on the stack and use beq.s to check whether it returns
TRUE or FALSE.
• If it is TRUE, we execute the relevant code and jump to the ret instruction.
• If it is FALSE, the next case value on the stack is checked.
• Finally, if none of the beq.s instructions result in TRUE, the default clause, which is
at the end of the switch constuct, is executed.
Just as we do not have the equivalent of the if statement in IL, we also do not have a pure
corresponding switch instruction in IL. The switch is more of a convenience to
programmers of C#. The rule that a case has to end with a break statement, do not apply
in IL.
This program demonstrates the use of the checked and unchecked operators and their
implementation in IL.
The fields b and c are initialised to a decimal value of 1000 or a hex value of Oxf4240 in
the constructor. Then, in the function vijay, they are put on the stack, and functions pqr
and xyz are called. These functions return values that are not subsequently used
anywhere. Thus, the pop instruction is used to remove them off the stack.
The function pqr does not achieve anything useful. The br.s instruction also does not
achieve anything of significance. This function uses the unchecked operator in C#, which
happens to be the default operator.
The function xyz only introduces a small variation: the mul instruction has been replaced
by the mul.ovf instruction. The term ovf is the short form for the word overflow. In case an
overflow occurs, the mul.ovf instruction will throw an exception.
Thus, overflow handling is done internally by employing IL instructions. If IL was unable to
provide for handling overflows, the C# compiler would have had to provide the code for
generation of an exception.
In conclusion, whenever we use the checked operator, the compiler tells IL to use the ovf
family of instructions, so that the program can check for an overflow and generate an
exception.
In the case of a constant, it does not matter whether a function uses the checked or
unchecked operators. This is because, constants are a compile time issue. They are
converted to actual constants by the compiler, as has oft been repeated.
The compiler actually multiples the constants x and y and replaces them with the value of
the resultant product. Thus, the mul operator does not make an appearance anywhere as
there is no trace of the checked operator.
It can be appreciated that the treatment of constants is different in C# and IL. So, given the
IL code, it is very difficult to use reverse engineering to arrive back at the original C# code.
Please note that most of the arithmetic operators in IL can be suffixed with .ovf thereby
ensuring that they check for overflow.
The bitwise left shift and right shift operators of C# are converted to instructions shl and
shr respectively.
• Every time we use the bitwise right shift operator, it is equivalent to dividing by 2.
• Every time we use the bitwise left shift operator, it is equivalent to multiplying by 2.
These instructions execute much faster than the division and multiplication instructions.
The C# compiler executes code as it sees it. It starts from left to right. It first encounters +
+i. The value of i is thus increased from 2 to 3.
The dup instruction of IL duplicates the value at the top of the stack. The stloc.0 assigns
the number 3 to i. Then the number 1 is added to the variable i, making its resultant value
4.
The div instruction now sees 3 and 4 on the stack and thus, divides 3 by 4. The final
answer is 0 or .75, depending upon the data type of i.
In programming languages like C, the result is not pre-determinable, but in C#, the order
of evaluation is very lucid and clear - it executes the code from left to right using the
principle of "first come first served".
The above example again demonstrates that the compiler is unambiguous about the order
of execution of code on a "first come first served basis". It builds on the earlier example.
The variable i is first placed on the stack and then incremented by one, making its value 1,
but the value 0 is placed on the stack. Thus x becomes zero. Thereafter, 1 is placed on the
stack and i is again incremented by 1, making its value 2. The value of the parameter y is
1. Finally, 2 is placed on the stack. Parameter z has the value 2 and the value of the
variable i now becomes 3.
The IL code is much easier to understand.
We have created a zzz like object as local V_0 and only one int32 representing the variable
i. The instruction ldc.i4.0 places the initial value of i on the stack. Then, stloc.1 assigns the
value 0 to i. When the function abc is called, the this pointer is placed on the stack using
ldloc.0.
Now the fun starts. The value of i, which is 0, is placed on the stack and duplicated using
the dup instruction. Thus, two zeroes are placed on the stack. Next, the number 1 is
placed on the stack and the add instruction adds this number to the 0 already on the
stack, resulting in the sum of 1. The numbers 1 and 0, which were present on the stack
earlier, are removed.
We store this value in i using ldloc.1 and place the new value 1 on the stack. We again use
dup to duplicate this value and put it on the stack and use the add instruction to add the
original and the duplicated values.
By now, the value of i is now 3 and the this pointer and the values 0, 1 and 2 are present
on the stack. Hence WriteLine shows 2 in abc.
All this IL code has been written by a compiler and not a human being. If you are not clear
about the above code, you can draw the stack diagrams.
The bitwise operator ~ complements the bits, converting the 0s to 1s and 1s to 0s. This
operator has a very simple equivalent in IL, which is the not instruction.
The remainder operator % is converted to the rem instruction in IL. Thus, you must have
noticed that, all the basic operators of C# have simple equivalent IL instructions.
The bitwise anding, oring and xoring are also supported in IL by the equivalent
instructions and, or and xor. Thus, IL has most of the instructions present in its
assembler.
In addition, it has a number of higher level constructs. However, there is no logical ANDing
and ORing in IL because, IL does not understand the logical values TRUE and FALSE.
C# to IL 5 Operator Overloading(操作符重载)的更多相关文章
- [置顶] operator overloading(操作符重载,运算符重载)运算符重载,浅拷贝(logical copy) ,vs, 深拷贝(physical copy)
operator overloading(操作符重载,运算符重载) 所谓重载就是重新赋予新的意义,之前我们已经学过函数重载,函数重载的要求是函数名相同,函数的参数列表不同(个数或者参数类型).操作符重 ...
- C++ operator overload -- 操作符重载
C++ operator overload -- 操作符重载 2011-12-13 14:18:29 分类: C/C++ 操作符重载有两种方式,一是以成员函数方式重载,另一种是全局函数. 先看例子 # ...
- 侯捷STL学习(四)--OOP-GP/操作符重载-泛化特化
C++标准库第二讲 体系结构与内核分析 第1-7节为第一讲 读源代码前的准备 第八节:源代码分布 C++基本语法 模板的使用 数据结构和算法 本课程主要使用:Gnu C 2.9.1与Gun C 4.9 ...
- 5.1 C++基本操作符重载
参考:http://www.weixueyuan.net/view/6379.html 总结: 操作符重载指的是将C++提供的操作符进行重新定义,使之满足我们所需要的一些功能. 长度运算符“sizeo ...
- (二) operator、explicit与implicit 操作符重载
有的编程语言允许一个类型定义操作符应该如何操作类型的实例,比如string类型和int类型都重载了(==)和(+)等操作符,当编译器发现两个int类型的实例使用+操作符的时候,编译器会生成把两个整 ...
- C++ operator关键字(重载操作符)(转)
operator是C++的关键字,它和运算符一起使用,表示一个运算符函数,理解时应将operator=整体上视为一个函数名. 这是C++扩展运算符功能的方法,虽然样子古怪,但也可以理解:一方面要使运算 ...
- C++中operator关键字(重载操作符)
operator是C++的关键字,它和运算符一起使用,表示一个运算符函数,理解时应将operator=整体上视为一个函数名. 这是C++扩展运算符功能的方法,虽然样子古怪,但也可以理解:一方面要使运算 ...
- 操作符重载operator
发现一篇好文: 转载: http://www.cnblogs.com/xiehongfeng100/p/4040858.html #include <iostream>#include & ...
- C++ operator关键字(重载操作符)
operator是C++的关键字,它和运算符一起使用,表示一个运算符函数,理解时应将operator=整体上视为一个函数名. 这是C++扩展运算符功能的方法,虽然样子古怪,但也可以理解:一方面 ...
随机推荐
- c算法:字符串查找-KMP算法
/* *用KMP算法实现字符串匹配搜索方法 *该程序实现的功能是搜索本目录下的所有文件的内容是否与给定的 *字符串匹配,如果匹配,则输出文件名:包含该字符串的行 *待搜索的目标串搜索指针移动位数 = ...
- ON 子句和 WHERE 子句的不同
原文: https://www.cnblogs.com/zjfjava/p/6041445.html 即使你认为自己已对 MySQL 的 LEFT JOIN 理解深刻,但我敢打赌,这篇文章肯定能让你学 ...
- do while
do while结构的基本原理和while结构是基本相同的,但是它保证循环体至少被执行一次.因为它是先执行代码,后判断条件,如果条件为真,继续循环.
- oracle中有关初始化参数文件的几个视图对比
涉及oracle中有关初始化参数文件的几个视图主要有:v$paraemter,v$parameter2,v$system_parameter,v$system_parameter2,v$spparam ...
- Charles安装及配置
安装包及jar包下载地址: 1.下载Charles Proxy v4.2.dmg镜像文件,双击打开,将Charles拖拽到Applications中,Mac中打开一次Charles后关掉. 2.将下载 ...
- SpringBoot(二)thymeleaf模板的引入
接着上一次的配置 1.在pom文件中添加thymeleaf模板的引入, <dependency> <groupId>org.springframework.boot</g ...
- python day08作业答案
1. a f=open('11.txt','r',encoding='utf-8') a=f.read() print(a) f.flush() f.close() b. f=open('11.txt ...
- CPU 架构 —— ARM 架构
linux 系统查看 CPU 架构命令: $ arch armv7l $ uname -m armv7l # -m:--machine # 进一步查看处理器信息 $ cat /proc/cpuinfo ...
- 【机器学习基础】SVM实现分类识别及参数调优(二)
前言 实现分类可以使用SVM方法,但是需要人工调参,具体过程请参考here,这个比较麻烦,小鹅不喜欢麻烦,正好看到SVM可以自动调优,甚好! 注意 1.reshape的使用: https://docs ...
- Python之路,第一篇:Python入门与基础
第一篇:Python入门与基础 1,什么是python? Python 是一个高层次的结合了解释性.编译性.互动性和面向对象的脚本语言. 2,python的特征: (1)易于学习,易于利用: (2)开 ...