ioquatix
ioquatix

Reputation: 1476

Is it possible to avoid copying arguments to a lambda function?

I'd like to manage file descriptors using a Handle, and I want to use lambda expressions to process them. I'd like to use RAII to manage the underlying file descriptors. One option is to handle invalid values for descriptors (e.g. -1). However, I'd prefer for a handle to always be valid.

I've found that I can't seem to avoid invoking the copy constructor at least once. Here is a working example:

#include <fcntl.h>
#include <unistd.h>
#include <functional>
#include <system_error>
#include <iostream>

class Handle
{
public:
    Handle(int descriptor) : _descriptor(descriptor) {}
    ~Handle()
    {
        std::cerr << "close(" << _descriptor << ")" << std::endl;
        ::close(_descriptor);
    }

    Handle(const Handle & other) : _descriptor(::dup(other._descriptor))
    {
        std::cerr << "dup(" << other._descriptor << ") = " << _descriptor << std::endl;
        if (_descriptor == -1) throw std::system_error(errno, std::generic_category(), "dup");
    }

    int descriptor() const { return _descriptor; }

private:
    int _descriptor;
};

Handle open_path(const char * path)
{
    return ::open("/dev/random", O_RDONLY);
}

void invoke(std::function<void()> & function)
{
    function();
}

int main(int argc, const char * argv[]) {
    // Using auto f = here avoids the copy, but that's not helpful when you need a function to pass to another function.
    std::function<void()> function = [handle = open_path("/dev/random")]{
        std::cerr << "Opened path with descriptor: " << handle.descriptor() << std::endl;
    };

    invoke(function);
}

The output of this program is:

dup(3) = 4
close(3)
Opened path with descriptor: 4
close(4)

I know that the handle is being copied because it's being allocated by value within the std::function, but I was under the impression std::function could be heap allocated in some cases, which would perhaps avoid the copy (I guess this is not happening though).

There are a number of options, e.g. heap allocation, orusing a sentinel value (e.g. -1) which is checked. However, I'd like to have an invariant that a handle is always valid. It's sort of a matter of style and invariants.

Is there any way to construct the handle within the stack frame of the std::function to avoid copying, or do I need to take a different approach?

Perhaps as an additional point: to what extent can we rely on std::function to avoid copying it's arguments when it's created?

Upvotes: 2

Views: 615

Answers (2)

AndyG
AndyG

Reputation: 41100

Relying on elision or moving with std::function is not enough. Since std::function is required to be copyable, there's always a chance you might copy your Handle by accident elsewhere.

What you need to do instead is to wrap your Handle in something that will not invoke the copy constructor on copy. An obvious choice is a pointer. And an obvious choice of pointer would be a manged one like std::shared_ptr.

I made a few changes to your Handle class for testing (print statements for dtor, ctor, copy ctor), so I'll show those first:

class Handle
{
public:
    Handle(int descriptor) : _descriptor(descriptor) {std::cerr<<"Default ctor, descriptor: " << _descriptor << std::endl;}
    ~Handle()
    {
        std::cerr << "Dtor. close(" << _descriptor << ")" << std::endl;
    }

    Handle(const Handle & other) : _descriptor(other._descriptor+1)
    {
        std::cerr << "Copy ctor. dup(" << other._descriptor << ") = " << _descriptor << std::endl;
    }

    int descriptor() const { return _descriptor; }

private:
    int _descriptor;
};

Next let's modify open_path to return a shared_ptr:

std::shared_ptr<Handle> open_path(const char * path)
{
    return std::make_shared<Handle>(0);
}

And then we'll make a slight modification to our lambda in main:

std::function<void()> function = [handle = open_path("/dev/random")]{
        std::cerr << "Opened path with descriptor: " << handle->descriptor() << std::endl;
};

Our output now becomes:

Default ctor, descriptor: 0
Opened path with descriptor: 0
Dtor. close(0)

Live Demo

Upvotes: 2

Vittorio Romeo
Vittorio Romeo

Reputation: 93304

First, let's get this out of the way: std::function is completely orthogonal to lambdas. I wrote an article, "passing functions to functions" that should clarify their relationship and illustrate various techniques that can be used to implement higher-order functions in modern C++.

Using auto f = here avoids the copy, but that's not helpful when you need a function to pass to another function.

I disagree. You can use a template in invoke or something like function_view (see LLVM's FunctionRef for a production-ready implementation, or my article for another simple implementation):

template <typename F>
void invoke(F&& function)
{
    std::forward<F>(function)();
}

void invoke(function_view<void()> function)
{
    function();
}

Upvotes: 5

Related Questions