Passing Iterators To Functions

When building contracts, it's ideal to use helper functions occasionally, in order to keep the code clean and readable.

While doing this, you will undoubtedly run into the following issue:

  • You fetch a table row

  • You need to pass that row into a helper function, either to simply read it, or to modify it

  • You then need access to the outcome of that helper function so you can continue your logic

This is a bit trickier than you might think, but after doing it once, it becomes pretty simple. I'll show you the common way to do it, and then I'll show you the way I do it

struct [[eosio::table, eosio::contract(CONTRACT_NAME)]] users {
	eosio::name		wallet;
	uint8_t  		status;				

	uint64_t primary_key() const { return wallet.value; }
};
using users_table = eosio::multi_index<"users"_n, users>;

void handle_status(const users_table::const_iterator& user_itr, const uint8_t& new_status){
	if( new_status != user_itr->status ){
		// do something
	}
}

ACTION mycontract::dosomething(const name& user, const uint8_t& status){
	users_table users_t = users_table( _self, _self.value );
	auto itr = users_t.require_find( user.value, "user not found" );
	uint8_t new_status = 3;
	handle_status( itr, new_status );
}

The above method is the most common approach that I've seen people use. However, there are times where you may want to mutate the data inside the helper function, and this becomes quite annoying because you can't directly modify a pointer (at least in my experience).

What I do to get around this, is to create a custom struct that matches the table struct. And a constructor to populate that struct with the data found in the table row. This struct can then be modified inside helper functions. I'll show you what I mean.

struct [[eosio::table, eosio::contract(CONTRACT_NAME)]] users {
	eosio::name		wallet;
	uint8_t  		status;				

	uint64_t primary_key() const { return wallet.value; }
};
using users_table = eosio::multi_index<"users"_n, users>;

struct user_struct {
	eosio::name  	wallet;
  	uint8_t 		status;

	user_struct(const users& u)
		: wallet(u.wallet),
		  status(u.status) {}
	
	user_struct() = default;		
};

// Make sure to pass by reference using the `&` sign, don't use `const`
void handle_status(user_struct& user, const uint8_t& new_status){
	if( new_status != user.status ){
		user.status = new_status;
	}
}

ACTION mycontract::dosomething(const name& user, const uint8_t& status){
	users_table users_t = users_table( _self, _self.value );
	auto itr = users_t.require_find( user.value, "user not found" );
	user_struct u = user_struct(*itr);   // dereference the iterator
	uint8_t new_status = 3;
	handle_status( u, new_status );

	users_t.modify(itr, same_payer, [&](auto &_row){
		_row.status = u.status;
	});
}

This approach allows you to create an object that has the exact same data as the table row. Then, you can modify that object inside of your helper functions. Finally, you need to make sure that you set the table data based on the new info after modifying the object.

The nice thing about this approach is that you can take it a step further - you can create a helper function that fetches the table row in the first place, and then another helper function to modify that row. All without needing to worry about dealing with iterators and running into errors left and right.

user_struct mycontract::get_user(const name& user){
	users_table users_t = users_table( _self, _self.value );
	auto itr = users_t.require_find( user.value, "user not found" );
	return user_struct(*itr); 	
}

void mycontract::modify_user(user_struct& user){
	users_table users_t = users_table( _self, _self.value );
	auto itr = users_t.require_find( user.value, "user not found" );	
	users_t.modify(itr, same_payer, [&](auto &_row){
		_row.status = user.status;
	});
}

ACTION mycontract::dosomething(const name& user, const uint8_t& status){
	user_struct u = get_user( user );
	uint8_t new_status = 3;
	handle_status( u, new_status );
	modify_user( u );
}

It should be noted that the lifecycle of certain pointer objects can be outlived by other functions where those pointers are referred to.

If that sounds like gibberish to you, basically it works like this:

  • You have some helper function to fetch an iterator

  • That helper function returns the iterator

  • That returned iterator may become invalidated by the time you actually try to use it

This problem does not exist when using a custom struct the way that I demonstrated above. This is because the custom struct is not an iterator, but rather a defined object that maintains its lifecycle throughout the scope of where it is defined.

When in doubt, go with the custom struct approach instead of dealing directly with an iterator. Then, just refetch the table row later when you need to modify it again.

There is obviously a slight performance implication here - fetching a table row twice is less performant than fetching it once. However, in my experience the tradeoff is worth it for the improved sanity and readability.

Last updated