一、C++中的函数重载机制
函数定义时的处理:
编译器的函数符号命名机制,C++对代码进行编译时会根据函数名、参数列表(参数类型、数量、顺序)等对函数进行重新命名;
函数调用时的处理:
(我曾经错误理解为调用时的处理和处理函数定义时类似,通过判断参数类型来得到函数名进行调用,但是这种粗暴的方式会有很多逻辑漏洞,比如未考虑到有默认参数的函数调用,未考虑到编译器自动类型转换的函数调用等)
在函数调用时,编译器如何判断调用的是哪个函数呢,大概过程如下:首先确定所考虑的重载函数集合,该函数集合被称为候选函数,一组候选函数就是一组同名函数;然后,从第一步候选函数中选择可行函数,可行函数的参数个数要与调用的函数参数个数相同,或者可行函数的参数多一些,多出来的部分都有默认值,然后根据参数类型的转换将被调用的函数实参转换成候选函数的实参,如果依照参数转换规则没有找到可行函数,则该调用时错误的;最后,根据上一步选出的可行函数中选出最佳可行函数,从函数实参类型到相应可行函数参数所用的转换都要划分等级,根据等级的划分选出最佳可行函数;
二、Lua中实现函数重载的思路
在C++的语言层面已经提供了对重载的支持,但是lua并没有,如果两个函数名相同,则后者会替换前者。
在lua中实现重载机制是相对简单的,因为lua是一门比较小巧的语言,它不会在语言层面支持很多特性,这就减少了重载机制设计的复杂度,比如,我们可以不用考虑默认参数,因为lua函数本身并不支持默认参数,只能自行扩展;我们甚至也不需要考虑类型转换,因为lua中的类型转换非常简单。
在实现上,我们可以使用一个表来定义一组重载函数,然后利用元方法__call来进行调用;
有一个麻烦的问题,我们如何在一个函数定义时拿到函数的参数列表,在静态代码下显然是无法做到的,因此需要选择替代性方案;
最简单方式就是每个函数的命名都统一一个规则,比如Add,对于两个数字:Number_Number,对于两个字符串采用如下命名:String_String,然后直接将函数存储到表里,在进行__call的时候,直接判断实参的类型得到要调用函数的签名,从而执行函数调用;一般来说,还需要一个默认函数,如果没有任何找到可行函数,则使用默认函数;
function Overload()
local mt = {}
local errorFunc = function()
print('no match function!!!')
end
mt.__call = function(self, ...)
local arg = {...}
local signatureTb = {}
for i = 1, select('#', ...) do
signatureTb[#signatureTb + 1] = type(arg[i])
end
local funcSign = table.concat(signatureTb, '_')
return (self[funcSign] or self.default)(...)
end
return setmetatable({default = errorFunc}, mt)
end
local Add = Overload()
Add.number_number = function(a, b)
local r = a + b
return r
end
Add.string_string = function(a, b)
local r = a .. b
return r
end
print(Add(2, 3))
print(Add('hello', 'world'))
-- 5
-- helloworld
上述方式的缺点在于,把原本编译器底层的内容直接放到语言层,如果参数过多,函数签名就会过长,可读性差,这种方式一般只适用于机器,不然,一旦修改了一个参数类型,就需要随之修改函数命名,修改错误就无法调用到对应的函数,太麻烦;
优化实现
我们应该让重载的函数命名更简便,更智能,它的参数匹配应该是自动的,但是这种也需要去指定参数类型及顺序,只不过不需要自己拼接,对于两个number,它的命名应该是tb.number.number形式,也就是使用嵌套的索引方式来处理;
对于__call元方法,它依然和上面保持一致,根据调用的参数自动拼接得到函数签名;
而对于重载函数表来说,它的__newindex元方法也和上面保持一致;
但是对于__index来说,就要有不一样的处理,比如对于tb.number.string,在默认情况下,tb.number如果不提前赋值为{},这个语句一定会报错,因此我们不能对nil进行索引;所以,在进行index时,就是要创建一个表,并记录这个key(用作最终的签名拼接),在最后一个string,它用到的其实是赋值元方法,通常我们需要在这个元方法内拼接所有的key得到签名,然后赋值;
如下所示,对于__index元方法,我们创建这样一个函数index,它将索引的key加入到签名表中,并返回一个表subTb1,subTb1的索引元方法仍然是index,而赋值元方法newindex则会将签名表中的key连接到一起,然后加入到签名函数表中,并进行函数的赋值;这样即使有多个索引,它就会继续执行index,直到遇到newindex结束;
这种将所有子表的index元方法设为同一个的也比较常见,它通常希望一个表内的所有子表都保持同一行为,比如嵌套的只读表等;
overload = function()
local tb = {}
local funcTb = {}
local signTb = {}
local index
local newindex
index = function(t, k)
local tb = {}
table.insert(signTb, k)
setmetatable(tb, {__index = index, __newindex = newindex})
rawset(t, k, tb)
return tb
end
newindex = function(t, k, v)
assert(type(v) == 'function')
table.insert(signTb, k)
funcTb[table.concat(signTb, '_')] = v
end
return setmetatable(tb, {
__call = function(t, ...)
local n = select('#', ...)
local typeTb = {}
for i = 1, n do
local argType = type(select(i, ...))
table.insert(typeTb, argType)
end
local signStr = table.concat(typeTb, '_')
local func = funcTb[signStr]
if func == nil then
print('no such overload func', signStr)
else
func(...)
end
end,
__index = function(t1, k1)
signTb = {}
return index(t1, k1)
end,
__newindex = function(t1, k1, v1)
signTb = {}
newindex(t1, k1, v1)
end
})
end
local add = overload()
add.number = print
add.number.number = function(a, b)
print(a + b)
end
add.string.string = function(a, b)
print(a .. b)
end
add(1, 2)
add('hello', 'world')
add('hello', 'world', 2)