Lua Tables — Deep & Shallow Copies

Introduction

Copying a Lua table using the assignment operator doesn’t copy anything! All you get is another pointer to the same memory block.

We can illustrate that using the following example:

local original = {a = 'alpha', b = 'beta'}
1local copy = original
2copy.a = 'GAMMA'
3print(original.a)
1
“Copy” the original table using a simple assignment.
2
Alter the copy.
3
Check the original and get the output, GAMMA!

This shows that original and copy refers to the same memory block.

table.clone & table.copy

If you have imported the lulu.table module as

require 'lulu.table'

Then, you can make copies of any Lua table using the following two methods:

table.clone(tbl)
Returns a shallow copy of tbl. It does not copy any metatable.

table.copy(tbl)
Returns a deep copy of tbl, including metatables.

Shallow vs. Deep

The clone and copy methods create a new empty table.

A shallow clone then inserts copies of the top-level elements from tbl into that new table.

On the other hand, a deep copy copies all the elements from the tbl argument by recursively visiting all sub-tables, sub-sub-tables, and on.

The methods give identical results for arguments without sub-tables.

Example: Simple Tables

1local orig = {a = 'alpha', b = 'beta'}
2local deep = table.copy(orig)
3local shallow = table.clone(orig)

4putln("original     = %t", orig)
putln("deep copy    = %t", deep)
putln("shallow copy = %t", shallow)

5deep.a = "GAMMA"
putln("After setting `deep.a to '%s'`", deep.a)
putln("original     = %t", orig)
putln("deep copy    = %t", deep)
putln("shallow copy = %t", shallow)

6shallow.b= "RHO"
putln("After setting `shallow. to '%s'`", shallow.b)
putln("original     = %t", orig)
putln("deep copy    = %t", deep)
putln("shallow copy = %t", shallow)
1
Here, orig is a simple Lua table with two key, value pairs and no sub-tables.
2
deep is a deep copy of orig, so we expect changes to deep not to change orig.
3
shallow is a shallow clone of orig, so changes to shallow might also affectorig`.
4
The examples on this page use Scribe for formatted printing.
5
Change to deep will only be reflected in deep no matter the structure of orig.
6
Because orig has no sub-tables, a change to shallow is only reflected in shallow.

When you run the program, you get:

Output: Simple Tables

original     = {['a'] = "alpha", ['b'] = "beta"}
deep copy    = {['a'] = "alpha", ['b'] = "beta"}
shallow copy = {['a'] = "alpha", ['b'] = "beta"}
After setting `deep.a to 'GAMMA'`
original     = {['a'] = "alpha", ['b'] = "beta"}
1deep copy    = {['a'] = "GAMMA", ['b'] = "beta"}
shallow copy = {['a'] = "alpha", ['b'] = "beta"}
After setting `shallow. to 'RHO'`
original     = {['a'] = "alpha", ['b'] = "beta"}
deep copy    = {['a'] = "GAMMA", ['b'] = "beta"}
2shallow copy = {['a'] = "alpha", ['b'] = "RHO"}
1
Only deep is changed.
2
Only shallow is changed.

In this example, the original table had no sub-tables, so there is no difference between deep copying and shallow cloning.

The results differ when we alter the example by putting the table values into sub-tables!

Example: Table with Sub-Tables

1local orig = {a = {'alpha'}, b = {'beta'}}
local deep = table.copy(orig)
local shallow = table.clone(orig)

putln("original     = %t", orig)
putln("deep copy    = %t", deep)
putln("shallow copy = %t", shallow)

deep.a[1] = "GAMMA"
2putln("After setting `deep.a[1] to '%s'`", deep.a[1])
putln("original     = %t", orig)
putln("deep copy    = %t", deep)
putln("shallow copy = %t", shallow)

shallow.b[1]= "RHO"
3putln("After setting `shallow.b[1] to '%s'`", shallow.b[1])
putln("original     = %t", orig)
putln("deep copy    = %t", deep)
putln("shallow copy = %t", shallow)
1
Here, we have spuriously introduced sub-tables instead of setting orig.a = "alpha" we have orig.a[1] = "alpha"etc.
2
Deep copies are independent, so this change will only affect deep.
3
Shallow clones are only independent at the top level so this change will affect both shallow and orig. Of course, it will not affect the independent deep.

Now, when you run the program, you get:

Output: Table with Sub-Tables

original     = {['a'] = {"alpha"}, ['b'] = {"beta"}}
deep copy    = {['a'] = {"alpha"}, ['b'] = {"beta"}}
shallow copy = {['a'] = {"alpha"}, ['b'] = {"beta"}}
After setting `deep.a[1] to 'GAMMA'`
original     = {['a'] = {"alpha"}, ['b'] = {"beta"}}
1deep copy    = {['a'] = {"GAMMA"}, ['b'] = {"beta"}}
shallow copy = {['a'] = {"alpha"}, ['b'] = {"beta"}}
After setting `shallow.b[1] to 'RHO'`
original     = {['a'] = {"alpha"}, ['b'] = {"RHO"}}
deep copy    = {['a'] = {"GAMMA"}, ['b'] = {"beta"}}
2shallow copy = {['a'] = {"alpha"}, ['b'] = {"RHO"}}
1
As expected, a change to a sub-element is only seen deep.
2
In contrast, a change to a sub-element is seen in the shallow copy and the original table.
The only native complex type in Lua is a table so every non-trivial data structure will have tables with sub-tables. For this reason, it is essential to understand the distinction between having multiple references by assignment, shallow cloning, and deep copying.

See Also

table.eq

Back to top