Safe Buffers

Disclaimer

This is merely an exercise. While it may work in production, your colleagues may be not so happy with it.

Passing C++ containers into C-style functions

When using libraries with C-style interfaces, we often see functions that accept an array of data, or a buffer as two parameters: pointer to the first element and length.

Classic example is

char *fgets(char *str, int n, FILE *stream)

If we’re going to call it, we’ll have something like

    char buffer[BUF_SIZE];
    while (fgets(buffer, BUF_SIZE, file))
        ...

or

    std::array<char, BUF_SIZE> buffer;
    while (fgets(buffer.data(), buffer.size(), file))
        ...

In C++ we usually have data types that provide both buffer and size, so wouldn’t it be nice to pass only one argument?

    std::array<char, BUF_SIZE> buffer;
    while (my_fgets(buffer, file))
        ...

Indeed, it would!

If you felt cringe at the very mention of such an idea, you may have it even worse further. Consider yourself warned.

Problem statement

Ok, so let’s formulate what we’re trying to achieve here:

For a function F get a wrapper function expand_containers<F> that will forward it’s arguments to F “as-is”, unless those are containers - those need to be expanded into two arguments, pointer and size.

Expressed as a C++ template, it’d look like

    template<auto func, typename... TActuals>
    auto expand_containers(TActuals&&... args) 
    {
        return func(expand_or_forward(std::forward<TActuals>(args)...));
    }

Will the code above work?

No, it won’t: we cannot make a function expand that will convert one type into two.

But we don’t need to expand types! We can expand the values of container arguments into tuples, and concatenate those tuples (using std::tuple_cat) with the values of other arguments. Then we can simply call the function with that tuple as an argument, using std::apply.

So, the code will look like this

    template<auto func, typename... TActuals>
    auto expand_containers(TActuals&&... args) 
    {
        auto tuple = std::tuple_cat(expand_or_forward(std::forward<TActuals>(args)...));
        return std::apply(func, std::move(tuple));
    }

Now we only need to define expand_or_forward!

First of all, the version for “normal” values:

template<typename TScalar>
std::tuple<TScalar> expand_or_forward(TScalar&& t)
{
    return { std::forward<TScalar>(t) };
}

then for containers:

template<typename TContainer, typename TData = decltype(TContainer{}::data())>
std::tuple<TData, size_t> expand_or_forward(TContainer& t)
{
    return  { t.data(), t.size() };
}

And why should’t we define it for C-style arrays of known size?

template<typename TItem, size_t N>
std::tuple<TItem*, size_t> expand_or_forward(TItem (&array)[N])
{
    return { array, N };
}

That’s all!

Performance

Is there a cost of this transformation (besides cognitive one)?

Well, we can look at the generated code!

All three major compilers (gcc, clang and MS VC++) inline expand_containers and everything in leaving only call to fgets, with a constant for buffer size, if it’s known at compile time. (reminder that on UNIX-like systems arguments are passed via registers rdi, rsi, rdx etc, while on Windows the registers are rcx, rdx, r8 etc.).

Conclusion

I’m not sure I’d recommend such wrappers for serious code, especially if the team members are not fluent in C++ template magic.

But is it fun? IMHO, yes.