Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VisualC++

Dynamically Sized Struct

5.00/5 (9 votes)
6 Nov 2014BSD1 min read 23.5K  
Simplifying the use of dynamically sized C structs

Introduction

It is quite common in C APIs to have structs that contain dynamically sized buffers. An initial call is made in which a size parameter is populated, after which a larger chunk of memory is allocated, the pertinent struct parameters are copied across, and a second call is made in which the data is retrieved.

This tip presents a generic way to eliminate many of the risks inherent in writing code calling such APIs.

Example of API Use

Consider the USB_NODE_CONNECTION_NAME struct, which is used in Windows to retrieve the link name of a connected USB hub.

C++
typedef struct _USB_NODE_CONNECTION_NAME {
  ULONG ConnectionIndex;
  ULONG ActualLength;
  WCHAR NodeName[1];
} USB_NODE_CONNECTION_NAME, *PUSB_NODE_CONNECTION_NAME;

Using only minimal error checking for brevity (don't try this at home), typical usage would look like this:

C++
bool GetUsbConnectionName(HANDLE hDevice, 
                          ULONG index, 
                          std::wstring& name)
{
    ULONG nBytes;
    
    // One struct of default size to query required size
    USB_NODE_CONNECTION_NAME connectionName;
    
    // And one dynamically created struct
    PUSB_NODE_CONNECTION_NAME connectionNameP;

    // 1. Initialise struct
    connectionName.ConnectionIndex = index;

    // 2. Query actual length
    BOOL success = DeviceIoControl(hDevice,
        IOCTL_USB_GET_NODE_CONNECTION_NAME,
        &connectionName,    // input data (e.g. index)
        sizeof(connectionName),
        &connectionName,    // output data (e.g. length)
        sizeof(connectionName),
        &nBytes,
        NULL);
        
    if (!success)
        return false;

    // 3. Allocate required memory
    size_t required = sizeof(connectionName) + 
                      connectionName.ActualLength -
                      sizeof(WCHAR);
    connectionNameP = (PUSB_NODE_CONNECTION_NAME)malloc(required);
    
    // 4. Initialise second struct
    connectionNameP->ConnectionIndex = index;
    
    // 5. Query name
    success = DeviceIoControl(hDevice,
        IOCTL_USB_GET_NODE_CONNECTION_NAME,
        connectionNameP,    // input data (e.g. index)
        required,
        connectionNameP,    // output data (e.g. name)
        required,
        &nBytes,
        NULL);
        
    if (!success)
        return false;

    // 6. Copy data (from the second struct, not the first)
    name = std::wstring(connectionNameP->NodeName, 
                        connectionNameP->NodeName + 
                        connectionName.ActualLength / sizeof(WCHAR));
                    
    // 7. Release memory
    free(connectionNameP);

    return true;
}

There are three problems with this approach: the struct we're using is initialised twice, we must remember to free the memory, and we have two structures to keep track of.

A Generic Struct

In C++, we can use the power of templates and managed memory to improve on the code above. We can use a std::vector<char> to take the place of a buffer created dynamically on the heap, and take advantage of the fact that if we make it larger, the existing data is unchanged.

C++
template <typename T>
class dynamic_struct
{
    // Actual memory in which the struct is held
    std::vector<char> buffer;
public:
    // Contained type
    typedef T Type;
    
    // Default constructor ensures minimum buffer size
    dynamic_struct()
    : buffer(sizeof(T))
    {}
    
    // Parameterised constructor for when the size is known
    dynamic_struct(std::size_t size)
    {
        resize(size);
    }

    // Change size of buffer allocated for struct
    void resize(std::size_t size)
    {
        if (size < sizeof(T))
            throw std::invalid_argument("Size too small for struct");
        buffer.resize(size, 0);
    }

    // Get current buffer size (never less than struct_size)
    std::size_t size() const
    {
        return buffer.size();
    }

    // Get struct template type size
    static std::size_t struct_size()
    {
        return sizeof(T);
    }

    // Access struct
    const T& get() const
    {
        return *reinterpret_cast<const T*>(&buffer.front());
    }

    // Access struct
    T& get() 
    {
        return *reinterpret_cast<T*>(&buffer.front());
    }
};

Example, Simplified

Using this handy class, the function to get the name can be simplified:

C++
bool GetUsbConnectionName(HANDLE hDevice, 
                          ULONG index, 
                          std::wstring& name)
{
    ULONG nBytes;
    dynamic_struct<USB_NODE_CONNECTION_NAME> connectionName;

    // 1. Initialise struct
    connectionName.get().ConnectionIndex = index;

    // 2. Query actual length
    BOOL success = DeviceIoControl(hDevice,
        IOCTL_USB_GET_NODE_CONNECTION_NAME,
        &connectionName.get(), // input data (e.g. index)
        connectionName.size(),
        &connectionName.get(), // output data (e.g. length)
        connectionName.size(),
        &nBytes,
        NULL);
        
    if (!success)
        return false;

    // 3. Allocate required memory
    size_t required = sizeof(connectionName) + 
                      connectionName.ActualLength -
                      sizeof(WCHAR);
    connectionName.resize(required);
    
    // 4. Query name
    success = DeviceIoControl(hDevice,
        IOCTL_USB_GET_NODE_CONNECTION_NAME,
        &connectionName.get(), // input data (e.g. index)
        connectionName.size(),
        &connectionName.get(), // output data (e.g. name)
        connectionName.size(),
        &nBytes,
        NULL);
        
    if (!success)
        return false;

    // 5. Copy data
    name = std::wstring(connectionName.get().NodeName, 
                        connectionName.get().NodeName + 
                        connectionName.get().ActualLength / sizeof(WCHAR));

    return true;
}

In this case, there is no risk of forgetting to initialise the second struct, no risk of getting confused about which struct to copy data from, and no risk of memory leaks, even in the presence of exceptions.

History

  • 6th November, 2014: Initial version
  • 7th November, 2014: Corrected template name from "t" to "T". Put template type in second example (went missing in copy-paste of code)

License

This article, along with any associated source code and files, is licensed under The BSD License