重载运算符的设计
大约 4 分钟
重载运算符的设计
重载运算符所考虑的问题
对于重载运算符来说,一般需要考虑:
运算符符号是什么
- 符号,如
++++
- 变量名(或称为标识符名),如
multiplus
- 符号,如
支持的运算符种类
- 前缀,如
+1
。一般有+-!&*
。 - 中缀,如
1+1
。 - 后缀,如
"{}"json
。 - 括号,如
f(a)
。一般有apply()
和get[]
。
- 前缀,如
支持重载的运算符是否有限
同时与重载运算符相结合的特性一般有:
- 成员函数或方法(包括扩展函数),单参数(函数名做中缀)调用时省略点和括号。例如
object.f(a)
写成object f a
。 - 全局函数,单参数(函数名做前缀)调用时省略括号。例如
f(a)
写成f a
。 - 函数的柯里化。例如
f(a)
写成f a
。在左结合的情况下,多参数函数可以写成f a b c
- 统一函数调用。例如
object.f()
写成f(object)
,把成员函数转变为全局函数使用。
其中成员函数中缀与函数的柯里化以及统一函数调用是矛盾的。
运算符的优先级问题。 如果要在语法分析阶段完成运算符优先级的处理的话,只能重载有限的符号,或者采取有限符号有优先级+无限符号按函数的优先级处理的方式,或者用已有符号作为优先级标识。
一些现成语言的方案
kotlin的方案是,可以重载有限个符号运算符,支持无限个变量名当中缀
class Infix {
init {
print("Firstly, ")
}
//成员函数中缀
infix fun println(str:String){
kotlin.io.println(str)
}
}
//扩展函数中缀
infix fun Infix.println(num:Int){
kotlin.io.println(num)
}
fun main(){
//中缀的具体实例
Infix() println "Hello World"
Infix() println 123
}
//输出:
//Firstly, Hello World
//Firstly, 123
cpp的方案是,可以重载有限个符号运算符,支持无限个变量名当字面量的后缀。(只支持unsigned long long和char[]类型有字面量后缀。)
import std;
class Infix {
public:
int value;
Infix(int _value) { value = _value; }
//成员方法中缀重载
auto operator+(Infix right) -> Infix {
return Infix(value + right.value);
}
//友元函数中缀重载(友元函数不是成员方法,但是 )
friend auto operator<<(std::ostream& os, const Infix& obj) -> std::ostream& {
os << "Hello, " << obj.value;
return os;
}
};
//全局中缀重载
constexpr auto operator|(const Infix& left, int&& right) -> int {
return left.value | right;
}
//字面量后缀重载
auto operator""out(unsigned long long i) -> unsigned long long {
std::cout << i << std::endl;
return i;
}
auto main() -> int {
//字面量后缀重载
auto i = Infix(2out);
// i|1 中缀符号重载,结果为3
auto j = Infix(i | 1);
// i+j 中缀符号重载,结果为Infix(5)
//<<符号重载,结果多了输出"Hello, "
std::cout << i + j;
return 0;
}
//输出:
//2
//Hello, 5
scala的方案是,可以无限个符号与变量运算符当中缀,有限个符号运算符当前缀。当一个运算符符号名称(如++++
)使用多个运算符时,将根据运算符的第一个字符来评估优先级[1] 。
case class Infix(val value:Int) {
//成员方法中缀
def +(right: Infix): Infix = Infix(value + right.value)
//成员方法单参数函数可以当作中缀运算符使用
infix //可以不用加infix
def need(right: Int): Int = value + right
override def toString: String = value.toString
}
//扩展函数中缀,而且可以用有限运算符的组合当作新运算符
extension (left:Infix)
def ++++( right: Infix): Infix = left + left + right + right
//单参数函数无法省略.和括号
def cancel(left: Infix):Infix = left
@main
def main(): Unit = {
val i = Infix(1)
val j = Infix(i need 2)
val k = cancel(i ++++ j)
println(k)
}
//输出:
//8
groovy的方案是,支持单参数函数调用不带括号当前缀,有限个符号运算符当中缀
println "Hello"
目前设想的方案
- 前缀有限符号运算符,前缀无限变量运算符(全局函数单参数调用时省略括号)
- 中缀有限符号运算符有优先级,无限符号运算符的优先级和函数相同(或者scala那样,例如
+++
与加号优先级一致;或者根据最后一个字符的优先级,例如+++*
与乘号优先级一致),没有中缀变量名运算符,(成员函数单参数调用时省略括号,但是不能省略点) - 后缀字面量无限变量名运算符
举一个例子:
func println(Infix i)->void{
//全局函数当前缀使用
}
func Infix.println(string str)->void{
//中缀,调用时保留`.`
}
func Infix.$++++(Infix right)->Infix{
//中缀,调用时可以忽略`.`
}
func Infix.$+()->Infix{
//有限符号前缀重载方案1,这个方案的优点是可以拿到this,拿到private变量
}
func $+(Infix i)->Infix{
//有限符号前缀重载方案2,这个方案的优点是和全局函数统一
}
func $int.out()->int{
//无参扩展函数作为字面量后缀重载,只有几个指定的字面量才能做
}
func main(){
var i = Infix(1);
var j = Infix(2);
println i;
(i ++++ j).println "ok";
}