重载运算符的设计

XiLaiTL大约 4 分钟

重载运算符的设计

重载运算符所考虑的问题

对于重载运算符来说,一般需要考虑:

  • 运算符符号是什么

    1. 符号,如++++
    2. 变量名(或称为标识符名),如multiplus
  • 支持的运算符种类

    1. 前缀,如+1。一般有+-!&*
    2. 中缀,如1+1
    3. 后缀,如"{}"json
    4. 括号,如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";
}

  1. https://docs.scala-lang.org/zh-cn/tour/operators.htmlopen in new window scala docs ↩︎

上次编辑于:
贡献者: XiLaiTL