Почему C ++ std :: function передает функтор по значению вместо универсальной ссылки? - PullRequest
0 голосов
/ 03 августа 2020

Конструктор std :: function выглядит так (по крайней мере, в libc ++):

namespace std {

template<class _Rp, class ..._ArgTypes>
function {
  // ...
  base_func<_Rp(_ArgTypes...)> __func;

public:
  template<typename _Fp>
  function(_Fp __f) : __func(std::move(__f)) {}

  template<typename _Fp>
  function& operator=(_Fp&& __f) {
    function(std::forward<_Fp>(__f)).swap(*this);
    return *this;
  }
};

}

Он предоставляет конструктор из произвольного функтора и оператор присваивания из произвольного функтора. Конструктор использует передачу по значению, но оператор присваивания использует передачу по универсальной ссылке.

Мой вопрос в том, почему конструктор std :: function не передает универсальную (пересылающую) ссылку в так же, как оператор присваивания? Например, он мог бы сделать:

namespace std {

template<class _Rp, class ..._ArgTypes>
function {
  // ...
  base_func<_Rp(_ArgTypes...)> __func;
public:
  template<typename _Fp>
  function(_Fp&& __f) : __func(std::forward<_Fp>(__f)) {}
  
  template<typename _Fp>
  function& operator=(_Fp&& __f) {
    function(std::forward<_Fp>(__f)).swap(*this);
    return *this;
  }
};

}

Мне любопытно, какое обоснование здесь по-разному трактует присваивание и конструктор. Спасибо!

1 Ответ

4 голосов
/ 03 августа 2020

Это то, что называется «параметром приемника». Параметр приемника - это параметр метода, который необходимо «взять» у вызывающего объекта и сохранить в объекте (как член данных). Вызывающему обычно не нужен / не используется объект после вызова.

Лучшая практика для параметра приемника - передать его по значению и перейти от него к объекту. Давайте разберемся, почему:

Вариант 1: передать по ссылке

class X; // expensive to copy type with cheap move

struct A
{
     X stored_x_;

     A(const X& x) : x_{x} {}
//                   ^~~~~
//                   this is always a copy
};

В этом случае всегда будет как минимум 1 копия, которую нельзя исключить.

Вариант 2: пройти по значению, а затем перейти от

class X; // expensive to copy type with cheap move

struct A
{
     X stored_x_;

     A(X x) : x_{std::move(x)} {}
//            ^~~~~~~~~~~~~~~~
//            this is now a move
};

Мы избавились от перемещения при инициализации A::x_, но у нас все еще есть копия при передаче параметра, или мы?

Если вызывающий поступает правильно, мы этого не делаем. У нас есть два случая: вызывающей стороне по-прежнему нужна копия переданного объекта (что довольно необычно и не является идиоматическим c). В этом случае да, копия будет сделана, но это потому, что этого требует вызываемый объект, а не из-за недостатка в конструкции нашего класса A.

Вызывающему объекту не нужен объект после проходя это. В этом случае он перемещает аргумент или, что еще лучше, передает prvalue, и, начиная с C ++ 17 с новыми правилами временной материализации, объект создается непосредственно как параметр:

Передайте xvalue

auto test()
{
    X x{};

    A a{std::move(x)}; // 2 moves (from arg to parameter and from parameter to `A::x_`)
};

Передайте prvalue

auto test()
{
    A a{X{}}; // just the move in the initialization of `A::x_`
}

Вариант 3: перегрузки ссылок lvalue и rvalue

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

class X; // expensive to copy type with cheap move

struct A
{
     X stored_x_;

     A(const X& x) : x_{x} {}
     A(X&& x) : x_{std::move(x)} {}
};

Ненужная сложность, которая вырывается, когда у вас есть несколько параметров приемника в 1 методе.

Вариант 4: передать по ссылке пересылки:

Опять же, возможно. Но у него могут быть некоторые тонкие, но довольно серьезные проблемы ios:

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

  • Это еще хуже для конструктора, поскольку теперь этот конструктор является жизнеспособным вариантом для конструктора копирования, который действительно может напортачить, потому что это лучше подходит для копии из неконстантного объекта.

Другая проблема в том, что его нельзя использовать всегда:

  • если вы хотите принять любой тип, который не является просто T, например, если X является шаблоном: template <class T> A(X<T>&& x) это не ссылка пересылки, а ссылка rvalue, и вам нужна перегрузка ссылки lvalue.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...