Skip to content

Enumerators

Although not used that frequently, Ruby supports enumerators that enable both internal and external iteration. The easiest way to create an enumerator is to not pass a block to an enumerable method. For example:

a = [1, 2, 3, 4, 5]

# Common way to iterate
a.each do |i|
  puts i
end

# Get an enumerator instead
enumerator = a.each

# Now use it
enumerator.map |i|
  i * 2
end

Rice has built-in support to returning enumerators for STL containers such as std::vector, std::map and std::unordered_map.

Implementing enumerators is tricky - and in fact requires a number of Rice features. So let's take a look at how enumerator support is implemented for std::vector.

Implementation

Let's start with looking at the code:

define_method("each", [](T& vector) -> const std::variant<std::reference_wrapper<T>, Object>
{
  if (!rb_block_given_p())
  {
    auto rb_size_function = [](VALUE recv, VALUE argv, VALUE eobj) -> VALUE
    {
      T* receiver = Data_Object<T>::from_ruby(recv);
      return detail::To_Ruby<size_t>().convert(receiver->size());
    };

    return rb_enumeratorize_with_size(detail::selfThread, Identifier("each").to_sym(), 0, nullptr, rb_size_function);
  }

  for (Value_T& item : vector)
  {
    VALUE element = detail::To_Ruby<Value_T>().convert(item);
    detail::protect(rb_yield, element);
  }

  return std::ref(vector);
});

Method Signature

Rice defines an each method to support Ruby's enumerable module. The vector is passed by reference to avoid a copy. The return type is a std::variant because the method can either return a Ruby enumerator or the vector.

Creating an Enumerator

If a block is not provided, the method returns an enumerator using rb_enumeratorize_with_size.

Yielding to a Block

The code iterates over each item by reference, wraps it in a Ruby object, and yields it to the block.

Returning Self

Returns a reference to the vector wrapped in std::reference_wrapper to enable method chaining.