Metatables and metamethods

Lua · Advanced · 14 min

Metatables are the mechanism behind classes, proxies, default values and operator overloading in Lua.

Every table can have a metatable: an ordinary table whose specially named fields, metamethods, define what happens when an operation isn't handled normally. Set it with setmetatable(t, mt) (which returns t) and read it with getmetatable(t).

The most important ones:

  • __index runs when a key is missing. If it is a table, Lua looks the key up there (and in that table's __index, and so on); if it is a function it is called with (t, key). This is how defaults and inheritance work.
  • __newindex runs when assigning to a missing key; use rawset inside it to actually store.
  • __add, __sub, __mul, __eq, __lt, __le, __concat, __unm, __len overload operators.
  • __call makes a table callable; __tostring controls tostring and print; __close and __gc handle cleanup.

rawget, rawset, rawequal and rawlen bypass metamethods, which you need inside them to avoid infinite recursion.

Strings share one metatable whose __index is the string table, which is why s:upper() works. Only tables (and full userdata from C) can have individual metatables set from Lua.

Metamethods are looked up with raw access in the metatable, so they must be present when the operation happens; adding __gc after setmetatable doesn't register it.

Example

local Vec = {}
Vec.__index = Vec
local function vec(x, y) return setmetatable({x = x, y = y}, Vec) end

Vec.__add = function(a, b) return vec(a.x + b.x, a.y + b.y) end
Vec.__eq = function(a, b) return a.x == b.x and a.y == b.y end
Vec.__tostring = function(v) return "(" .. v.x .. ", " .. v.y .. ")" end
function Vec.len(v) return math.sqrt(v.x ^ 2 + v.y ^ 2) end

local a = vec(3, 4)
print(tostring(a + vec(1, 1)))   --> (4, 5)
print(a == vec(3, 4))            --> true
print(a:len())                   --> 5.0 (found through __index)

local defaults = setmetatable({}, {__index = function(t, k) return 0 end})
defaults.hits = defaults.hits + 1
print(defaults.hits, defaults.misses)   --> 1	0

local readonly = setmetatable({}, {__newindex = function() error("read-only", 2) end})
print(pcall(function() readonly.x = 1 end))  --> false	...read-only

Remember

  • __index handles missing keys: a table to delegate, a function to compute
  • Arithmetic, comparison, call and tostring are all overridable
  • Use rawget/rawset inside metamethods to avoid recursion

Watch out for

  • Expecting __index to run for keys that exist: it only fires on missing keys.

Check yourself

  1. When does a table's __index metamethod run?
  2. Why use rawset inside __newindex?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.lua.org/manual/5.4/manual.html#2.4