C API string array returns in C++

Posted in software by Christopher R. Wirz on Mon Feb 12 2018



When when a function returns a type of unknown size, you have a few options. If you use C++, you can pass a vector by reference - and once the function performs its operations, the vector will have changed in size.


#include <stdio.h>
#include <stdio.h>
#include <string>
#include <vector>
#include <string.h>
#include <malloc.h>

/// <summary>
///     Adds strings to a vector
/// </summary>
void GetStrings(std::vector<std::string>& v){
    v.push_back("Hi");
    v.push_back("There");
    v.push_back("Pal");
}


But what if you are using a C-style API. This means no access to the standard library in the function declaration (use whatever you have available in the definition). In the case of getting back a collection of strings (which in C are char* character arrays), this means you have to pass a triple pointer!

The following example shows that passing a pointer to an array of character arrays allows the client to receive a collection of varying-size.


/// <summary>
///     Returns an array of character arrays (C strings)
///     and the array's length
/// </summary>
void GetStrings(char *** strings, int & size, bool clientFrees = false){
    std::vector<std::string> sts; // stack allocated, no need to free
    GetStrings(sts); // From the method above
    size = sts.size();

    (*strings) = (char**)malloc(sizeof(char*) * size);
    for (int n = 0; n < size; n++){
        if (clientFrees){
            (*strings)[n] = (char*)malloc(sizeof(char) * (sts[n].size()+1));
            strncpy((*strings)[n], sts[n].c_str(), sts[n].size()+1);
        }
        else {
            (*strings)[n] = (char*)sts[n].c_str();
        }
    }
}

The downside is the malloc taking place inside the function. The client has to free the array of character arrays (char**) when done.

Freeing the elements of the array is optional, however. This has to do with the lifecycle of the array. Because the c_str() method returns a pointer to the character array, the actual value can change if not read quickly. If the client wants to hold on to the array loner, the malloc may be appropriate.

In either case, the client will still have to free the returned array - but the freeing the individual elements is optional - as shown in the code below.


int main()
{
    // change this to see what happens when the client
    // must free resources
    bool clientFrees = true;

    // define the out parameters
    int size = 0;
    char** strs = NULL;

    // Get the strings
    GetStrings(&strs, size, clientFrees);

    // Inspect the out parameters
    printf("size = %d\n", size);
    for (int n = 0; n < size; n++){
        printf("%s\n", (char*)strs[n]);
        if (clientFrees){
            free((char*)strs[n]);
        }
    }

    // No control over the malloc, must free
    free(strs);

    return 0;
}

Running the code gives the following output.


size = 3
Hi
There
Pal

As seen, the size of the array is successfully passed - and the elements are read. The client frees the returned array to prevent a memory leak - but optionally frees the elements.

Go ahead. I recommend you try this yourself.

Now here's a question: why not just try it like this, such that nothing has to be freed:


/// <summary>
///     Returns an array of character arrays (C strings)
///     and the array's length, but with an error
/// </summary>
void GetStringsAgain(char *** strings, int & size){
    std::vector<std::string> sts;
    GetStrings(sts);
    size = sts.size();
    *strings = (char**)sts.data();
    // can also write as
    // *strings = ((char**)(&sts[0]));
}

Well, as it turns out, the results are somewhat strange.


int main()
{
    // define the out parameters
    int size = 0;
    char** strs = NULL;

    // Get the strings
    GetStringsAgain(&strs, size);

    // Inspect the out parameters
    printf("size = %d\n", size);
    for (int n = 0; n < size; n++){
        printf("%s\n", *strs);
        strs++;
    }

    return 0;
}

Will lead to the following output:


size = 3

There
Pal

In which the first element is not provided. Try it for yourself.