Reputation: 1476
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
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)
Upvotes: 2
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