【问题标题】:Applying C++11 move semantics to bound functions将 C++11 移动语义应用于绑定函数
【发布时间】:2016-10-10 02:49:34
【问题描述】:

我有一些现有的 C++98 代码,它们使用 boost::functionboost:bind 进行异步回调。一些相关的简化代码片段包括:

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);
    }
};

在构建WriteOperation 时需要一份副本,但除此之外,我会尽量避免使用副本,因为它们可能非常昂贵。

我正在考虑如何最好地在 C++11 世界中编写它。显而易见的是,WriteOperation 构造函数在内部将其参数复制到其字段,因此应使用自动复制习语:

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))
{}

(当然,裸露的new 应该替换为unique_ptr,但这是一个附带问题。)

但是,鉴于Device::Write 的当前实现,我认为这实际上并没有任何好处,所以这也应该改变。我的问题是我真的没有看到一个好的方法来做到这一点。根据this advice,我有三种选择:

  1. 声明多个重载(一个带有const&amp;,一个带有&amp;&amp;)——但由于它有两个参数,这两个参数都可以从移动语义中受益,这将需要四个重载——呈指数级恶化对于具有更多参数的方法。此外,这会导致代码重复或将代码分散到其他方法上,从而影响可读性。

  2. 按值传递和移动(类似于WriteOperation 构造函数)。当主体总是进行复制时,这可能是最简洁的选择,如果实际调用了 WriteOperation 构造函数,则为 true,但是如果省略的部分包含可能在不构造 WriteOperation 的情况下返回的逻辑怎么办?在这种情况下有一个浪费的副本。

  3. 模板和完善转发。这需要一个丑陋的 SFINAE hack,它会混淆 Intellisense 并损害可读性(或者更糟糕的是,使参数类型不受约束),并且需要将实现放入标头中,这有时是不可取的。 而且它会干扰类型转换,例如。寻找 std::string 的 SFINAE enable_if is_same 不会接受 const char * 文字,而原始的 const&amp; 版本会。

我错过了什么吗?有更好的解决方案吗?还是这只是移动语义没有任何区别的情况?


一个相关案例:

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);
}

这一切看起来应该没问题,没有复制也不需要移动语义。我再次不确定 ReadHandler 是否传递给 Read

【问题讨论】:

  • “因为这有两个参数,这两个参数都可以从移动语义中受益”,是那些?在您的示例代码中,您将所有三个参数都视为受益于移动语义。
  • Device::Write 只有两个参数。 WriteOperation 构造函数有第三个参数,但由于这是通过 shared_from_this() 传递的,所以无论如何它都会被复制。
  • 嗯,Device::Write 被调用的可能性有多大,将临时变量作为实际参数,优化这种情况有多重要?
  • 如果你跟踪调用链足够远,它会在一个局部变量处结束(在调用之后不再使用),所以可能总是这样,假设那些中间层也移动了。至于它有多重要,这是我想弄清楚的。

标签: c++ c++11 move-semantics boost-bind boost-function


【解决方案1】:

大致顺序:

如果复制和移动一样昂贵,请通过const&amp; 获取。

如果您可靠地保留一份副本,并且搬家很便宜,请按价值计算。

如果做不到这一点,您可以将其填充到标题中,并且可以使用 sfinae 或不受约束的模板,使用转发引用。

否则,如果参数数量有限,请编写每个重载。这是2^n的参数个数,所以最好不要太多。在内部转发到基于转发引用的实现。

如果做不到这一点,你真的需要效率吗?

如果失败,请键入擦除到“T 的创建者”。

template<class T, using Base=std::function<T()>>
struct creator_of: Base
{
  template<class U,
    std::enable_if_t<std::is_constructible<T, U&&>{},int> =0
  >
  creator_of(U&&u):
    Base([&]()->T{ return std::forward<U>(u); })
  {}
  template<class U,
    std::enable_if_t<std::is_constructible<T, std::result_of_t<std::decay_t<U>()>{},int> =0
  >
  creator_of(U&&u):
    Base(std::forward<U>(u))
  {}

  creator_of(creator_of&&)=default;
  creator_of():
    Base([]()->T{return {};}}
  {}
};

根据需要进行扩充。 creator_of&lt;std::string&gt; 可以从可以构造 std::string 的事物构造,也可以从返回 std::string 的函数对象构造。

您可以在内部拨打()一次。

(代码未编译,但设计合理。)

【讨论】:

  • “搬家很便宜”是一个重要的考虑因素。我可以在按值构造函数习语上找到的大多数讨论都没有提到,如果类型定义了一个没有显式(删除或不删除)移动构造函数的复制构造函数(即大多数现有的 C++98 类型),它可以执行两个副本,因此默认情况下在这些类型上更差。
  • 这并没有解决 OP 的担忧“但是如果省略的部分包含可能在不构造 WriteOperation 的情况下返回的逻辑怎么办?在这种情况下会浪费副本”。此外,creator_of 代码需要用一个真实的使用示例来充实。
  • @cheers 如果未调用 creator_of,则不执行复制。这怎么不能解决问题?除了“您是否需要效率”和“可靠地保留副本”解决方案之外,每个后备方案都存在类似情况。
  • 在我看来,尽管手头的情况不一定如此,但通常是否存储值的决定取决于参数值。复制一个参数只是为了检查它是高成本的。我认为对于现代 C++,您的方法比我的方法更好,至少如果有人相信编译器会优化这些函子,但它还没有解决这个问题。
  • @cheer 如果你取消了“take a lambda”功能,添加.inspect() 方法很容易。
【解决方案2】:

为避免组合爆炸,同时支持在被调用函数内部证明不需要参数副本的情况,您可以推迟复制。

这实际上是一种惰性求值方案,但仅适用于这种特殊情况。

对于像下面这样的支持类,需要敏锐地意识到一个实例可能只是持有一个指向短期调用者对象的指针。即,不要将 Lazy_copy_ 移动到超过调用期限的存储中。

#include <iostream>
#include <new>
#include <string>           // std::string
#include <utility>          // std::move, std::forward
using namespace std;

//------------------------------------ Machinery:

#ifdef TRACE
    inline void trace( string const& s )
    {
        clog << ": " << s << endl;
    }
#else
    inline void trace( string const& ) {}
#endif

struct Emplace {};

template< class Item >
class Lazy_copy_
{
private:
    Item const* p_callers_item_;
    union{ Item item_copy_; };      // Left uninitialized if p_callers_item_.

    void ensure_is_copy()
    {
        if( p_callers_item_ )
        {
            ::new( &item_copy_ ) Item( *p_callers_item_ );
            trace( "ensure_is_copy: made copy" );
            p_callers_item_ = nullptr;
        }
    }

public:
    auto item() const
        -> Item const&
    { return (p_callers_item_? *p_callers_item_ : item_copy_); }

    auto item_copy()
        -> Item&
    {
        ensure_is_copy();
        return item_copy_;
    }

    ~Lazy_copy_()
    {
        if( not p_callers_item_ ) { item_copy_.Item::~Item(); }
    }

    Lazy_copy_( Lazy_copy_ const& other )
        : p_callers_item_( other.p_callers_item_ )
    {
        if( p_callers_item_ )
        {
            ensure_is_copy();
        }
        else
        {
            ::new( &item_copy_ ) Item( other.item_copy_ );
            trace( "<init>( Lazy_copy ): made copy" );
        }
    }

    Lazy_copy_( Lazy_copy_&& other )
        : p_callers_item_( other.p_callers_item_ )
    {
        if( not p_callers_item_ )
        {
            ::new( &item_copy_ ) Item( move( other.item_copy_ ) );
            trace( "<init>( Lazy_copy&& ): moved" );
        }
    }

    Lazy_copy_( Item const& item )
        : p_callers_item_( &item )
    {}

    Lazy_copy_( Item&& temp_item )
        : p_callers_item_( nullptr )
        , item_copy_( move( temp_item ) )
    {
        trace( "<init>( Item&& ): moved" );
    }

    template< class... Args >
    Lazy_copy_( Emplace, Args&&... args )
        : p_callers_item_( nullptr )
        , item_copy_( forward<Args>( args )... )
    {
        trace( "<init>( Emplace, Args... ): Created item from constructor args" );
    }
};

//------------------------------------ Example usage:

struct Thingy
{
    string a, b, c;

    void foo(
        Lazy_copy_<string>      arg_one,
        Lazy_copy_<string>      arg_two,
        Lazy_copy_<string>      arg_three
        )
    {
        if( arg_one.item() == "no_copy" )
        {
            return;     // The case of no copying needed.
        }
        a = move( arg_one.item_copy() );
        b = move( arg_two.item_copy() );
        c = move( arg_three.item_copy() );
    }
};

auto main()
    -> int
{
    Thingy  x;
    string a = "A", b = "B", c = "C";

    trace( "Call with copying:" );
    x.foo( string( "a" ), b, c );
    trace( "" );
    trace( "Call without copying: " );
    x.foo( string( "no_copy" ), b, c );
}

使用 TRACE 定义构建时的输出:

: 复制调用: : ( Item&& ): 已移动 : ensure_is_copy: 复制 : ensure_is_copy: 复制 : :无需复制即可调用: : ( Item&& ): 已移动

【讨论】:

  • 虽然只是经过const&amp;,但这是一个额外的动作。
  • @Miral:我没看到。什么情况?
  • 使用const&amp; 时,在分配给字段时没有复制/移动条目和复制分配。使用上面的代码,对于右值 arg,在入口处有一个移动构造(到Lazy_copy_),然后是对字段的移动赋值;对于左值 arg,入口没有任何内容,然后是复制构造,然后是移动分配。不确定是否允许编译器将其省略为单个复制分配。如果不是,那是左值的额外移动,但右值更好(前提是移动两次比复制一次便宜)。
  • FWIW,在 VS2013 中无法编译,因为它不允许联合成员具有复制构造函数。在 VS2015 中,它会编译,但不会忽略复制分配。
  • @Miral:哦,你说得对。我想这对我来说已经很晚了。但是,如果您可以忍受左值 arg 表达式的额外移动,那么您可以避免右值 arg 表达式的额外复制。 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-11-11
  • 2014-08-16
  • 2019-01-02
  • 2013-01-15
  • 1970-01-01
  • 2011-09-25
  • 2014-03-18
相关资源
最近更新 更多