One framework I created that used ICOP had the following classes:
IoObj - Base class for I/O
IoHnd - IFS handle based
File - OS file access
NetSocket - Network socket
NetIpClnt/NetIpSvc, NetTCP*, NetUdp*, NetIrda*, NetBth*, ...
NetFPT*, NetNNTP*, ...
WSAOVERLAPPED
IoOL - Base overlapped; adds flags, timestamps, type, control, buffer
NetIoOLAddr - specialied for sendto/recfrom, contains remote address
NetIoOLAccept - specialized for handling AcceptEx
IoCP - I/O completion port
The IoCP had the main posting functions and GetQueuedCompletionStatus loop. To post a msg on an IoCP you'd pass an IoObj* and IoOL*. The IoObj* would be the key, the IoOL* the overlapped (per call). The IoCP GetQueuedCompletionStatus loop would call (something like) IoObj->Msg(IoOL*). With the above you can use one or more IoCP instances to handle any combination of files, sockets, or other IoObj based objects (e.g. jobs/tasks). I found both having key and OL convenient. Without an explicit key param you'd have to add it as state to your extended OL ... which you may want anyways.
...cmk The idea that I can be presented with a problem, set out to logically solve it with the tools at hand, and wind up with a program that could not be legally used because someone else followed the same logical steps some years ago and filed for a patent on it is horrifying. - John Carmack