2016-10-10 3 views
4

У меня есть некоторый код C++ 98, который использует для асинхронных обратных вызовов boost::function и boost:bind. Некоторые соответствующие упрощенные фрагменты кода включают в себя:Применение семантики перемещения C++ 11 к связанным функциям

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& и один с &&) - но так как это имеет два параметра, оба из которых может извлечь пользу из семантики хода, это потребовало бы четыре перегрузки - экспоненциально хуже для методов с большим количеством параметров. Кроме того, это приводит к дублированию кода или рассеянию кода по дополнительным методам, что может ухудшить читаемость.

  2. Передача по значению и перемещение (аналогично конструктору WriteOperation). Это, пожалуй, самый чистый вариант, когда тело всегда делает копию, что верно, если на самом деле вызывается конструктор WriteOperation, но что, если разрешенный раздел содержит логику, которая может вернуться без построения WriteOperation? В этом случае есть потерянная копия.

  3. Шаблон и идеальный вперед. Это требует уродливого хакера SFINAE, который смущает Intellisense и ухудшает читаемость (или, что еще хуже, оставляя тип параметра безусловным) и требует, чтобы реализация была помещена в заголовок, что иногда нежелательно. И он мешает преобразованию типов, например. a SFINAE enable_ifis_same ищет std::string не принимается буквами const char *, а оригинальная версия const& будет.

Я что-то не хватает? Есть ли лучшее решение? Или это просто случай, когда семантика перемещения не имеет никакого значения?


Связанный случай:

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.

+0

«так как это имеет два параметра, оба из которых могут извлечь пользу из семантики ходу», которые являются те? В вашем примере кода вы обрабатываете все три аргумента, используя семантику перемещения. –

+0

'Device :: Write' имеет только два параметра. Конструктор 'WriteOperation' имеет третий параметр, но поскольку он передается' shared_from_this() ', он всегда будет скопирован в любом случае. – Miral

+0

Хорошо, насколько вероятно, что 'Device :: Write' будет вызываться с временными в качестве фактических аргументов и насколько важно оптимизировать этот случай? –

ответ

1

В грубом порядке:

Если копия так же дорого, как двигаться, взять его const&.

Если вы надежно храните копию, а двигаться дешево, возьмите на себя стоимость.

В противном случае вы можете записать его в заголовке и в порядке с 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 {};}} 
    {} 
}; 

дополнение по мере необходимости. A creator_of<std::string> может быть построен из объектов, которые могут построить std::string, или из функционального объекта, возвращающего std::string.

Внутренне вы можете позвонить по номеру () после этого.

(код не компилируется, но дизайн звук.)

+1

«Движение дешево» является важным соображением. Большинство обсуждений, которые я могу найти в конструкторе по значению idiom, не могут упомянуть, что он может выполнять две копии, если тип определяет конструктор копирования без явного (удалённого или не) перемещающего конструктора (т. Е. Большинства существующих типов C++ 98) , и, таким образом, по умолчанию эти типы хуже. – Miral

+0

Это не касается озабоченности OP », но что, если разрешенная секция содержит логику, которая может вернуться без создания WriteOperation?В этом случае есть потерянная копия ».Кроме того, код' creator_of' нуждается в компиляции с реальным примером использования. –

+0

@cheers, если 'creator_of' не вызывается, копия не выполняется. Как это не решает проблему? Аналогичный случай справедлив для каждого из резервов, за исключением случаев «нужна ли вам эффективность» и «надежно хранить копии». – Yakk

-1

Чтобы избежать комбинаторного взрыва, поддерживая при этом случай, когда он оказывается внутри вызываемой функции, которые не нужны никакие аргументы копии, вы можете отложить в копирование.

Это эффективно ленивая схема оценки, но только для этого особого случая.

С классом поддержки, подобным приведенному ниже, нужно быть в курсе, что экземпляр может просто удерживать указатель на объект недолгого звонящего. I.e., не перемещайте 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 определены:

 
: Call with copying: 
: <init>(Item&&): moved 
: ensure_is_copy: made copy 
: ensure_is_copy: made copy 
: 
: Call without copying: 
: <init>(Item&&): moved 
+0

Это дополнительный шаг только при передаче 'const &'. – Miral

+0

@ Мираль: Я этого не вижу. В каком случае? –

+0

С 'const &' при присваивании поля копировать/перемещать запись и копировать нельзя. С приведенным выше кодом для rvalue arg существует перемещение-конструкция при входе (в 'Lazy_copy_'), а затем перемещение-задание в поле; и для lvalue arg нет ничего на входе, затем copy-construct, а затем-move-assign. Не уверен, что компилятору разрешено исключить это вместо одного назначения копии. Если нет, это один дополнительный ход для lvalues, но rvalues ​​лучше (при условии, что перемещение вдвое дешевле, чем копирование один раз). – Miral

Смежные вопросы