Конструктор с использованием std:: forward

Насколько я знаю, два общих способа эффективного реализации конструктора в С++ 11 используют два из них

Foo(const Bar& bar) : bar_{bar} {};
Foo(Bar&& bar)      : bar_{std::move(bar)} {};

или только один в порядке

Foo(Bar bar) : bar_{std::move(bar)} {};

с первым параметром, обеспечивающим оптимальную производительность (например, надеюсь, что единственная копия в случае lvalue и одно перемещение в случае rvalue), но для перегрузки N требуется 2 N тогда как второй вариант нуждается только в одной функции за счет дополнительного перемещения при передаче в lvalue.

В большинстве случаев это не должно сильно влиять на результат, но, безусловно, ни один из них не является оптимальным. Однако можно также сделать следующее:

template<typename T>
Foo(T&& bar) : bar_{std::forward<T>(bar)} {};

Это имеет недостаток, позволяющий допускать переменные возможных нежелательных типов как параметр bar (который является проблемой, я уверен, легко разрешается с помощью специализированной специализации), но в любом случае производительность является оптимальной, а код растет линейно с количество переменных.

Почему никто не использует что-то вроде передового для этой цели? Разве это не самый оптимальный способ?

Ответы

Ответ 1

Люди делают идеальные конструкторы вперед.

Есть затраты.

Во-первых, стоимость заключается в том, что они должны быть в файле заголовка. Во-вторых, каждое использование имеет тенденцию приводить к созданию другого конструктора. В-третьих, вы не можете использовать синтаксис инициализации {} для объектов, из которых вы строите.

В-четвертых, он плохо взаимодействует с конструкторами Foo(Foo const&) и Foo(Foo&&). Он не заменит их (из-за языковых правил), но он будет выбран над ними для Foo(Foo&). Это можно исправить небольшим количеством шаблона SFINAE:

template<class T,
  std::enable_if_t<!std::is_same<std::decay_t<T>, Foo>{},int> =0
>
Foo(T&& bar) : bar_{std::forward<T>(bar)} {};

который теперь больше не предпочтительнее Foo(Foo const&) для аргументов типа Foo&. Пока мы находимся в этом, мы можем сделать:

Bar bar_;
template<class T,
  std::enable_if_t<!std::is_same<std::decay_t<T>, Foo>{},int> =0,
  std::enable_if_t<std::is_constructible<Bar, T>{},int> =0
>
Foo(T&& bar) :
  bar_{std::forward<T>(bar)}
{};

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

Следующее, что вам нужно сделать, - либо поддерживать конструкцию стиля {} конструкции bar, либо кусочную конструкцию, либо конструкцию varargs, где вы переходите в панель.

Вот вариант varargs:

Bar bar_;
template<class T0, class...Ts,
  std::enable_if_t<sizeof...(Ts)||!std::is_same<std::decay_t<T0>, Foo>{},int> =0,
  std::enable_if_t<std::is_constructible<Bar, T0, Ts...>{},int> =0
>
Foo(T0&&t0, Ts&&...ts) :
  bar_{std::forward<T0>(t0), std::forward<Ts>(ts)...}
{};
Foo()=default;

С другой стороны, если мы добавим:

Foo(Bar&& bin):bar_(std::move(bin));

теперь поддерживаем синтаксис Foo( {construct_bar_here} ), что приятно. Однако это не требуется, если у нас уже есть вышеуказанный вариант (или подобная кусочная конструкция). Тем не менее, иногда список инициализаторов хорош для пересылки, особенно если мы не знаем тип bar_ при написании кода (например, generics):

template<class T0, class...Ts,
  std::enable_if_t<std::is_constructible<Bar, std::initializer_list<T0>, Ts...>{},int> =0
>
Foo(std::initializer_list<T0> t0, Ts&&...ts) :
  bar_{t0, std::forward<Ts>(ts)...}
{};

поэтому, если bar является std::vector<int>, мы можем сделать Foo( {1,2,3} ) и в итоге {1,2,3} внутри bar_.

На этом этапе вам стоит задаться вопросом: "Почему я просто не написал Foo(Bar)". Неужели так дорого перемещать bar?

В общем коде библиотеки -экспекции вы захотите зайти так далеко, как указано выше. Но очень часто ваши объекты известны и дешевы для перемещения. Поэтому напишите действительно простой, верный, Foo(Bar) и сделайте все с помощью tomfoolery.

Существует случай, когда у вас есть N переменных, которые не являются дешевыми для перемещения, и вы хотите эффективности, и вы не хотите помещать реализацию в файл заголовка.

Затем вы просто пишете создателя стирания bar, который берет все, что может быть использовано для создания bar либо напрямую, либо через std::make_from_tuple и сохранит создание на более позднюю дату. Затем он использует RVO для прямой сборки bar на месте в целевом местоположении.

template<class T>
struct make {
  using maker_t = T(*)(void*);
  template<class Tuple>
  static maker_t make_tuple_maker() {
    return [](void* vtup)->T{
      return make_from_tuple<T>( std::forward<Tuple>(*static_cast<std::remove_reference_t<Tuple>*>(vtup)) );
    };
  }
  template<class U>
  static maker_t make_element_maker() {
    return [](void* velem)->T{
      return T( std::forward<U>(*static_cast<std::remove_reference_t<U>*>(velem)) );
    };
  }
  void* ptr = nullptr;
  maker_t maker = nullptr;
  template<class U,
    std::enable_if_t< std::is_constructible<T, U>{}, int> =0,
    std::enable_if_t<!std::is_same<std::decay_t<U>, make>{}, int> =0
  >
  make( U&& u ):
    ptr( (void*)std::addressof(u) ),
    maker( make_element_maker<U>() )
  {}
  template<class Tuple,
    std::enable_if_t< !std::is_constructible<T, Tuple>{}, int> =0,
    std::enable_if_t< !std::is_same<std::decay_t<Tuple>, make>{}, int> =0,
    std::enable_if_t<(0<=std::tuple_size<std::remove_reference_t<Tuple>>{}), int> = 0 // SFINAE test that Tuple is a tuple-like
    // TODO: SFINAE test that using Tuple to construct T works
  >
  make( Tuple&& tup ):
    ptr( std::addressof(tup) ),
    maker( make_tuple_maker<Tuple>() )
  {}
  T operator()() const {
    return maker(ptr);
  }
};

В коде используется функция С++ 17, std::make_from_tuple, которую относительно легко написать в С++ 11. В С++ 17 гарантированный elision означает, что он даже работает с недвижущимися типами, что действительно круто.

Живой пример.

Теперь вы можете написать:

Foo( make<Bar> bar_in ):bar_( bar_in() ) {}

и тело Foo::Foo может быть перемещено из файла заголовка.

Но это более сумасшествие, чем вышеупомянутые альтернативы.

Опять же, рассмотрели ли вы просто запись Foo(Bar)?