I have some existing C++98 code that makes use of boost::function and boost:bind for asynchronous callbacks. Some relevant simplified fragments of the code include:
typedef boost::function<void (boost::system::error_code, size_t)> WriteHandler;
struct WriteOperation
{
WriteOperation(const boost::shared_ptr<IDevice>& device,
const std::string& data, const WriteHandler& handler)
: m_Device(device), m_Data(data), m_Handler(handler) {}
private:
boost::shared_ptr<IDevice> m_Device;
std::string m_Data;
WriteHandler m_Handler;
void Complete()
{
boost::system::error_code ec;
size_t len;
...
Async::Post(boost::bind(m_Handler, ec, len));
}
};
struct Device : public IDevice
{
void Write(const std::string& data, const WriteHandler& callback)
{
...
Async::Start(new WriteOperation(shared_from_this(), data,
boost::bind(&Device::HandleWrite, this, handler, _1, _2)));
}
private:
void HandleWrite(const WriteHandler& callback,
boost::system::error_code ec, size_t len)
{
...
callback(ec, len);
}
};
There is one required copy, when constructing the WriteOperation, but other than that I'm trying to avoid copies since they can be quite expensive.
I'm pondering how this should best be written in the C++11 world. The obvious low-hanging fruit is that the WriteOperation constructor internally copies its arguments to its fields, so should use the automatic copying idiom:
WriteOperation(boost::shared_ptr<IDevice> device,
std::string data, WriteHandler handler)
: m_Device(std::move(device)), m_Data(std::move(data)), m_Handler(std::move(handler))
{}
(And of course the bare new should be replaced with unique_ptr, but that's a side issue.)
However I don't think this actually gains anything given the current implementation of Device::Write, so that ought to change too. My problem is that I don't really see a good way to do it. According to this advice, I have three options:
Declare multiple overloads (one with
const&and one with&&) -- but since this has two parameters, both of which could benefit from move semantics, this would require four overloads -- getting exponentially worse for methods with more parameters. Additionally this leads to either code duplication or scattering the code over additional methods, which can impair readability.Pass by value and move (similar to the
WriteOperationconstructor). This is perhaps the cleanest option when the body always makes a copy, which is true if theWriteOperationconstructor is actually called, but what if the elided section contains logic that might return without constructing aWriteOperation? There's a wasted copy in this case.Template and perfect forward. This requires an ugly SFINAE hack that confuses Intellisense and impairs readability (or worse, leaving the parameter type unconstrained), and requires that the implementation be put into the header, which is sometimes undesirable. And it interferes with type conversions, eg. a SFINAE
enable_ifis_samelooking forstd::stringwill not accept aconst char *literal, while the originalconst&version will.
Am I missing something? Is there a better solution? Or is this just a case where move semantics don't make any difference?
A related case:
typedef boost::function<void (boost::system::error_code, const std::string&)> ReadHandler;
void Read(const ReadHandler& callback)
{
... boost::bind(&Device::HandleRead, this, callback, _1, _2) ...
}
void HandleRead(const ReadHandler& callback,
boost::system::error_code ec, const std::string& data)
{
...
callback(ec, data);
}
This all seems like it should be ok, with no copying and no need for move semantics. And yet again I'm unsure about the ReadHandler passed to Read.
In rough order:
If copy is just as expensive as move, take it by
const&.If you reliably keep a copy, and move is cheap, take by value.
Failing that, you can stuff it in a header and are ok with sfinae or unconstrained templates, use forwarding references.
Failing that, if there are a limited number of arguments, write each overload. This is
2^nin the number of parameters, so there better not be too many. Forward internally to a forwarding reference based implementation.Failing that, do you really need efficiency here?
Failing that, type erase to "creator of
T".augment as needed. A
creator_of<std::string>can be constructed from things that can construct astd::string, or from a function object returning astd::string.Internally you can call
()once on it.(Code not compiled, but design is sound.)