Operator Overloading with Metamethods

· 6 min read · Updated March 18, 2026 · intermediate
metatables oop metamethods

One of Lua’s most powerful features is its ability to let you redefine how operators work with your own data types. Through metatables and metamethods, you can make tables respond to arithmetic operations, comparisons, and even function calls. This tutorial walks you through everything you need to know to start overloading operators in your Lua code.

Understanding Metatables and Metamethods

Every table in Lua can have an associated table called its metatable. The metatable contains special functions called metamethods that define behavior when certain operations are performed on the table. Think of a metatable as a table’s “personality” that tells Lua how to handle various operations.

When you write a + b in Lua, the interpreter normally expects both a and b to be numbers. But what if a is a vector represented as a table? Without customization, you’d get an error. With metatables, you can define what “adding” two vectors means.

Setting Up Your First Metatable

To use metamethods, you need three components: a table to hold your data, a metatable containing your custom functions, and the setmetatable function to connect them.

-- Create a table representing a 2D vector
local vector = {x = 10, y = 20}

-- Create a metatable with metamethods
local VectorMeta = {
    __add = function(a, b)
        return {x = a.x + b.x, y = a.y + b.y}
    end
}

-- Attach the metatable to our vector
setmetatable(vector, VectorMeta)

-- Now we can "add" vectors!
local v2 = {x = 5, y = 10}
local result = vector + v2

print(result.x, result.y)  -- Output: 15  30

This example creates a simple vector system where the + operator combines two vectors by adding their components together. The __add metamethod is one of seven arithmetic metamethods available.

Arithmetic Metamethods

Lua provides seven metamethods for arithmetic operations:

MetamethodOperation
__add+
__sub-
__mul*
__div/
__mod%
__pow^
__unmunary -

Here’s a more complete example that implements a fraction class with multiple arithmetic operations:

local Fraction = {}
Fraction.__index = Fraction

function Fraction.new(numerator, denominator)
    local self = setmetatable({}, Fraction)
    self.n = numerator
    self.d = denominator
    return self
end

function Fraction:simplify()
    -- Simple gcd for simplification
    local a, b = self.n, self.d
    while b ~= 0 do
        a, b = b, a % b
    end
    self.n = self.n / a
    self.d = self.d / a
    return self
end

local FractionMeta = {
    __add = function(a, b)
        local n = a.n * b.d + b.n * a.d
        local d = a.d * b.d
        return Fraction.new(n, d):simplify()
    end,
    
    __mul = function(a, b)
        local n = a.n * b.n
        local d = a.d * b.d
        return Fraction.new(n, d):simplify()
    end,
    
    __tostring = function(f)
        return f.n .. "/" .. f.d
    end
}

setmetatable(Fraction, {__call = function(_, n, d) return Fraction.new(n, d) end})
setmetatable(Fraction.prototype or {}, FractionMeta)

-- Usage
local f1 = Fraction(1, 2)
local f2 = Fraction(1, 3)
local sum = f1 + f2
print(sum)  -- Output: 5/6

local product = f1 * f2
print(product)  -- Output: 1/6

This fraction implementation demonstrates how multiple arithmetic metamethods work together to create a cohesive numeric type.

Comparison Metamethods

Lua supports three comparison metamethods: __eq for equality, __lt for less-than, and __le for less-than-or-equal. Notably, Lua does not provide __ge (greater-than-or-equal) — the interpreter automatically derives it by negating the __le result.

local Person = {}
Person.__index = Person

function Person.new(name, age)
    return setmetatable({name = name, age = age}, Person)
end

local PersonMeta = {
    __eq = function(a, b)
        return a.name == b.name and a.age == b.age
    end,
    
    __lt = function(a, b)
        return a.age < b.age
    end,
    
    __le = function(a, b)
        return a.age <= b.age
    end,
    
    __tostring = function(p)
        return p.name .. " (age " .. p.age .. ")"
    end
}

setmetatable(Person, {__index = PersonMeta})

-- Create some people
local alice = Person.new("Alice", 30)
local bob = Person.new("Bob", 25)
local charlie = Person.new("Charlie", 30)

-- Comparison operators now work!
print(alice == charlie)  -- Output: true (same name and age)
print(bob < alice)       -- Output: true (bob is younger)
print(alice <= charlie)  -- Output: true (same age)

-- Sorting works automatically
local people = {alice, bob, charlie}
table.sort(people)
for _, p in ipairs(people) do
    print(p)
end
-- Output: Bob (age 25), Alice (age 30), Charlie (age 30)

The sorting example shows how implementing __lt enables your custom types to work with Lua’s built-in table.sort function.

Other Useful Metamethods

Beyond arithmetic and comparison, Lua provides several other metamethods for common operations:

  • __concat — defines the .. (concatenation) operator
  • __len — defines the # (length) operator
  • __call — makes a table callable like a function
  • __tostring — controls how the table appears in print and tostring
  • __index — customizes field access (like a getter)
  • __newindex — customizes field assignment (like a setter)

Here’s an example combining several of these:

local Stack = {}
Stack.__index = Stack

function Stack.new()
    return setmetatable({items = {}}, Stack)
end

function Stack:push(item)
    table.insert(self.items, item)
end

function Stack:pop()
    return table.remove(self.items)
end

local StackMeta = {
    __call = function(s)
        return s:pop()
    end,
    
    __len = function(s)
        return #s.items
    end,
    
    __tostring = function(s)
        return "Stack(" .. #s.items .. " items)"
    end
}

setmetatable(Stack, {__call = function(_, ...) return Stack.new(...) end})

-- Usage
local myStack = Stack()
myStack:push("first")
myStack:push("second")
myStack:push("third")

print(#myStack)       -- Output: 3
print(myStack)        -- Output: Stack(3 items)

local item = myStack()  -- Calls __call, which pops
print(item)            -- Output: second
print(#myStack)        -- Output: 2

This stack implementation demonstrates how __call lets tables behave like functions, while __len enables the # operator and __tostring provides readable output.

Common Pitfalls

When working with metatables, keep these gotchas in mind:

Plain tables don’t support operator overloading. If you create a table without setting a metatable, arithmetic operations will fail. Always use setmetatable(yourTable, yourMetaTable).

Metamethods are looked up on the metatable, not the table itself. The operation triggers the metamethod from the metatable, regardless of which table performs the operation.

Not all operations have metamethods. For example, there’s no __gt — use __lt with negated logic instead. Lua 5.4 defines 19 metamethods total.

See Also

Summary

Metatables and metamethods are the foundation of operator overloading in Lua. By attaching a metatable to your table and defining appropriate metamethods, you can make your custom types work naturally with Lua’s operator syntax. Whether you’re building vectors, fractions, complex numbers, or game-specific data structures, metamethods let you write expressive, readable code that feels native to the language.

The key steps are: create your data table, define a metatable with the desired metamethods, and connect them with setmetatable. From there, the possibilities are endless.