Practical Perfect Forwarding

When I first started learning about perfect forwarding, I had a lot of trouble making the leap from academic descriptions of r-value references to usage patterns that would help me make my code faster and more robust. I experimented with code that used r-value references, and used the debugger to learn how they worked under the covers. This really helped me understand what was possible and allowed me to start using perfect forwarding in the game I’m currently working on. There were some misunderstandings I discovered along the way. I figured it might be useful for others that are just starting with perfect forwarding to talk about the usage patterns that have been useful for me. This is not an exhaustive list, it’s just what I’ve ended up using.

Cheaply moving an object

There are often times where I need to cheaply move an object. Sometimes the object has a lot of data in it, and after moved, I was going to discard the object anyways.

struct player
{
    std::unordered_map<guid, player_item> m_inventory;
    // other player details
};

struct player_item
{
    guid            m_guid;
    std::string     m_itemName;
    std::vector<player_item_attrib>  m_itemAttribs;
    void*           m_pItemSpecificBlob;
    // other player item details.
};

void player::loot_ship_debris(debris* pDebris)
{
    std::pair<guid, player_item> p;
    pDebris->loot_item(&p.second);
    p.first = p.second.m_guid;
    m_inventory.insert(p);
    // discard p.
}

In the above example, I’m populating a player_item object by looting ship debris, adding it to the inventory map (which performs a copy), and then discarding the temporary variable. Since I’m discarding the player_item after inserting it into the map, copying the data is pretty wasteful.

There are several traditional ways I might avoid this cost, for example I might make loot_item return a pointer to an item rather than populate the item, and use unique_pointers rather than objects in the unordered_map. This requires that I perform an additional allocation for every item, however. Alternatively I could determine the guid before looting the item, and emplace-insert before looting, although that could impact my game’s modularity. Yet another alternative might be to implement my own container class that can be inserted into the hash table without an allocation, although now I have to spend time writing a container class.

void player::loot_ship_debris(debris* pDebris)
{
    std::pair<guid, player_item> p;
    pDebris->loot_item(&p.second);
    p.first = p.second.m_guid;
    m_inventory.insert(std::move(p));
    // discard p.
}

Perfect forwarding offers a new alternative. It allows me to move the contents of the player_item to another player_item object rather than copying it. Unlike the copy, the move can be destructive, stealing ownership of the data from the old player_item object. When I reach the end of the function and destroy the pair, there is nothing substantial left in it.

In order to tell player_item how to move itself, I need to implement a “move” constructor and “move” assignment operator. This allows me to specifically define the move behavior, and is especially important if I have rules about how ownership of some of the data should be transferred.

player_item::player_item(player_item&& src)
    : m_guid(src.m_guid)
    , m_itemName(std::move(src.m_itemName))
    , m_itemAttribs(std::move(src.m_itemAttribs))
{
    m_pItemSpecificBlob = src.m_pItemSpecificBlob;
    src.m_pItemSpecificBlob = nullptr;
}

player_item& operator =(player_item&& src)
{
    m_guid = src.m_guid;
    m_itemName = std::move(src.m_itemName);
    m_itemAttribs = std::move(src.m_itemAttribs);
    m_pItemSpecificBlob = src.m_pItemSpecificBlob;
    src.m_pItemSpecificBlob = nullptr;   
    return *this; 
}

If I get rid of “m_pItemSpecificBlob”, and all the objects contained in player_item implement move operators, then I can skip implementing my own move operator by specifying the “default” keyword. This tells the compiler to implement a sensible move operator.

struct player_item
{
    player_item(player_item&& src) = default;
    player_item& operator =(player_item&& src) = default;
}

Lastly, in the name of efficiency, there may be situations where I want to be absolutely sure my object is never copied, it is only moved. I can accomplish this by marking the copy constructor and assignment operator with the delete keyword. Once the delete keyword is specified, any code that attempts to copy the object will generate a compile error, allowing me to quickly locate and fix this code.

struct player_item
{
    player_item(const player_item& src) = delete;
    player_item(player_item&& src) = default;
    player_item& operator =(const player_item& src) = delete;
    player_item& operator =(player_item&& src) = default;
}

Efficiently using STL containers

When STL first came out, move semantics didn’t exist, and so objects were always copied, never moved. This made STL really difficult to use efficiently, especially with vectors. Inserting/deleting elements in a vector requires elements to be shuffled up/down. This literally resulted in a copy/destroy of every element that was shuffled.

using player_item_attribs = std::vector<player_item_attrib>;
using player_items = std::vector<player_item>;

With perfect forwarding, things like shuffling elements, reallocating, rehashing, and sorting can all use the move operator if it’s implemented. Deleting the copy constructor & assignment operator can provide an extra level of safety to ensure things are being moved, not copied.

In addition, when adding items to containers we can also avoid the copy of the object as it is inserted to the container.

To-do list iteration

There are a number of places in my game where I process a to-do list. Some to-do items can be processed right now, while others cannot be processed until the next frame. I expect that most items will be processed immediately.

std::deque<todo> m_toDos;

void process()
{

    std::deque<todo>::iterator itToDo = m_toDos.begin();
    while (itToDo != m_toDos.end())
    {
        if (process(*itToDo))
        {
            itToDo = m_toDos.erase(itToDo);
        }
        else
        {
            ++itToDo;
        }
    }
}

Erasing in the middle of a deque is fairly inefficient. I could alternatively use a std::list, although that involves more allocations.

std::deque<todo> m_toDos;

void process()
{
    std::deque<todo> toDos = std::move(m_toDos);
    for (todo& t : toDos)
    {
        if (!process(t))
        {
            m_toDos.push_back(std::move(t));
        }
    }
}

Moves allow a new alternative. Moving the entire container is very cheap because inside the deque object, it simply swaps pointers. I can avoid erasing in the middle of the deque, and the individual todos that can’t be processed can be moved as well.

This also works well in the case where “process” adds to the to-do list, and I only want to process the todos that were originally on the list when the frame began.

Ownership clarity in APIs

There are many cases where data is passed into a helper function, and it’s unclear what the ownership semantics are. There are some typical API conventions, but there are often exceptions, and so it’s tough to assume. This can make it slower to understand how an API will work, and lead to misunderstandings and bugs.

void Send(const message& msg)
{
    // caller owns the message contents.
}

void Send(message& msg)
{
    // caller probably owns the message contents.
}

void Send(message* pMessage)
{
    // caller probably owns the message contents.
}

void Send(message** ppMessage)
{
    // callee might steal the message.
}

void Receive(message** ppMessage)
{
    // callee probably gives a message to caller.
}

Perfect forwarding gives us a way to set a clear expectation that ownership is being handed off to function.

void Send(message&& msg)
{
    // callee can do whatever it wants with message, don't 
    // expect msg to be in a useable state after Send() returns.
}

LootShipDebrisRequestMessage msg;
Send(std::move(msg));

With this API, the caller of the function must explicitly call std::move() when calling Send() to avoid a compile error. This makes the ownership semantics obvious to anyone reviewing either the caller and the callee code.

There are a couple of additional details worth mentioning about this handoff semantic. I learned the first one the hard way.

  1. The callee is not required to steal the object passed in. If the callee does not steal the object, then the data will remain in the object and the caller should clean the object up. This calling semantic gives the callee the opportunity to steal the object, and sets clear expectations for the caller that the callee might steal it.
  2. If the callee wants to hand off an object to a helper fuction, it must call std::move() again when calling the helper function. Once inside the callee, the object is treated like a regular reference.
void Send(message&& msg)
{
    SendInternal(std::move(msg));
}

Conclusion

These are just some of the practical uses of perfect forwarding I’ve been using in the game I’m working on. I hope these usage patterns will you save some time and write faster, more maintainable code !