containers

working with containers in sol2

Containers are objects that are meant to be inspected and iterated and whose job is to typically provide storage to a collection of items. The standard library has several containers of varying types, and all of them have begin() and end() methods which return iterators. C-style arrays are also containers, and sol2 will detect all of them for use and bestow upon them special properties and functions.

Note

Please note that c-style arrays must be added to Lua using lua["my_arr"] = &my_c_array; or lua["my_arr"] = std::ref(my_c_array); to be bestowed these properties. No, a plain T* pointer is not considered an array. This is important because lua["my_string"] = "some string"; is also typed as an array (const char[n]) and thusly we can only use std::reference_wrappers or pointers to the actual array types to work for this purpose.

container detection

containers are detected by the type trait sol::is_container<T>. If that turns out to be true, sol2 will attempt to push a userdata into Lua for the specified type T, and bestow it with some of the functions and properties listed below. These functions and properties are provided by a template struct sol::container_traits<T>, which has a number of static Lua C functions bound to a safety metatable. If you want to override the behavior for a specific container, you must first specialize sol::is_container<T> to drive from std::true_type, then override the functions you want to change. Any function you do not override will call the default implementation or equivalent. The default implementation for unrecognized containers is simply errors.

You can also specialize sol::is_container<T> to turn off container detection, if you find it too eager for a type that just happens to have begin and end functions, like so:

not_container.hpp
struct not_container {
        void begin() {

        }

        void end() {

        }
};

namespace sol {
        template <>
        struct is_container<not_container> : std::false_type {};
}

This will let the type be pushed as a regular userdata.

Note

Pushing a new usertype will prevent a qualifying C++ container type from being treated like a container. To force a type that you’ve registered/bound as a usertype using new_usertype or new_simple_usertype to be treated like a container, use sol::as_container.

container overriding

If you want it to participate as a table, use std::true_type instead of std::false_type from the containter detection example. and provide the appropriate iterator and value_type definitions on the type. Failure to do so will result in a container whose operations fail by default (or compilation will fail).

If you need a type whose declaration and definition you do not have control over to be a container, then you must override the default behavior by specializing container traits, like so:

specializing.hpp
struct not_my_type { ... };

namespace sol {
        template <>
        struct is_container<not_my_type> : std::true_type {};

        template <>
        struct container_traits<not_my_type> {

                ...
                // see below for implemetation details
        };
}

The various operations provided by container_traits<T> are expected to be like so, below. Ability to override them requires familiarity with the Lua stack and how it operates, as well as knowledge of Lua’s raw C functions. You can read up on raw C functions by looking at the “Programming in Lua” book. The online version’s information about the stack and how to return information is still relevant, and you can combine that by also using sol’s low-level stack API to achieve whatever behavior you need.

Warning

Exception handling WILL be provided around these particular raw C functions, so you do not need to worry about exceptions or errors bubbling through and handling that part. It is specifically handled for you in this specific instance, and ONLY in this specific instance. The raw note still applies to every other raw C function you make manually.

container operations

Below are the many container operations and their override points for container_traits<T>. Please use these to understand how to use any part of the implementation.

operation lua syntax container_traits<T> extension point stack argument order notes/caveats
set c:set(key, value) static int set(lua_State*); 1 self 2 key 3 value
  • if value is nil, it performs an erase in default implementation
  • if this is a sequence container and it support insertion and key,is an index equal to the size of the container,+ 1, it will insert at,the end of the container (this is a Lua idiom)
index_set c[key] = value static int index_set(lua_State*); 1 self 2 key 3 value
  • default implementation calls “set”
  • if this is a sequence container and it support insertion and key is an index equal to the size of the container + 1, it will insert at the end of the container (this is a Lua idiom)
get v = c:get(key) static int get(lua_State*); 1 self 2 key
  • can return multiple values
  • default implementation increments iterators linearly for non-random-access
index_get v = c[key] static int index_get(lua_State*); 1 self 2 key
  • can only return 1 value
  • default implementation just calls “get”
  • if key is a string and key is one of the other member functions, it will return that member function rather than perform a lookup / index get
find c:find(target) static int find(lua_State*); 1 self 2 target
  • target is a value for non-lookup containers (fixed containers, sequence containers, non-associative and non-ordered containers)
erase c:erase(target) static int erase(lua_State*); 1 self 2 target
  • for sequence containers, target is an index to erase
  • for lookup containers, target is the key type
  • uses linear incrementation to spot for sequence containers that do not have random access iterators (std::list, std::forward_list, and similar)
  • invalidates iteration
insert c:insert(target, value)   1 self 2 target 3 key
  • for sequence containers, target is an index, otherwise it is the key type
  • inserts into a container if possible at the specified location
add c:add(key, value) or c:add(value) static int add(lua_State*); 1 self 2 key/value 3 value
  • 2nd argument (3rd on stack) is provided for associative containers to add
  • ordered containers will insert into the appropriate spot, not necessarily at the end
size #c static int size(lua_State*); 1 self
  • default implementation calls .size() if present
  • otherwise, default implementation uses std::distance(begin(L, self), end(L, self))
clear c:clear() static int clear(lua_State*); 1 self
  • default implementation provides no fallback if there’s no clear operation
begin n/a static int begin(lua_State*, T&); n/a
  • called by default implementation
end n/a static int end(lua_State*, T&); n/a
  • called by default implementation
pairs   static int pairs(lua_State*); 1 self
  • implement if advanced user only that understands caveats
  • override begin and end instead and leave this to default implementation if you do not know what __pairs is for or how to implement it and the next function
  • works only in Lua 5.2+
  • calling pairs( c ) in Lua 5.1 / LuaJIT will crash with assertion failure (Lua expects c to be a table)
ipairs   static int ipairs(lua_State*); 1 self
  • implement if advanced user only that understands caveats
  • override begin and end instead and leave this to default implementation if you do not know what __ipairs is for or how to implement it and the next function
  • works only in Lua 5.2, deprecated in Lua 5.3 (but might still be called in compatibiltiy modes)
  • calling ipairs( c ) in Lua 5.1 / LuaJIT will crash with assertion failure (Lua expects c to be a table)

Note

If your type does not adequately support begin() and end() and you cannot override it, use the sol::is_container trait override along with a custom implementation of pairs on your usertype to get it to work as you want it to. Note that a type not having proper begin() and end() will not work if you try to forcefully serialize it as a table (this means avoid using sol::as_table and sol::nested, otherwise you will have compiler errors). Just set it or get it directly, as shown in the examples, to work with the C++ containers.

container classifications

When you serialize a container into sol2, the default container handler deals with the containers by inspecting various properties, functions, and typedefs on them. Here are the broad implications of containers sol2’s defaults will recognize, and which already-known containers fall into their categories:

container type requirements known containers notes/caveats
sequence erase(iterator) push_back/insert(value_type) std::vector std::deque std::list std::forward_list
  • find operation is linear in size of list (searches all elements)
  • std::forward_list has forward-only iterators: set/find is a linear operation
  • std::forward_list uses “insert_after” idiom, requires special handling internally
fixed lacking push_back/insert lacking erase std::array<T, n> T[n] (fixed arrays)
  • regular c-style arrays must be set with std::ref( arr ) or &arr to be usable
ordered key_type typedef erase(key) find(key) insert(key) std::set std::multi_set
  • container[key] = stuff operation erases when stuff is nil, inserts/sets when not
  • container.get(key) returns the key itself
associative, ordered key_type, mapped_type typedefs erase(key) find(key) insert({ key, value }) std::map std::multi_map  
unordered same as ordered std::unordered_set std::unordered_multiset
  • container[key] = stuff operation erases when stuff is nil, inserts/sets when not
  • container.get(key) returns the key itself
  • iteration not guaranteed to be in order of insertion, just like in C++ container
unordered, associative same as ordered, associative std::unordered_map std::unordered_multimap
  • iteration not guaranteed to be in order of insertion, just like in C++ container