std::move или std::forward при назначении универсального конструктора переменной-члену в C++

Рассмотрим следующие классы foo1 а также foo2

template <typename T>
struct foo1
{
    T t_;

    foo1(T&& t) :
        t_{ std::move(t) }
    {
    }
};

template <typename T>
struct foo2
{
    foo1<T> t_;

    foo2(T&& t) :
        t_{ std::forward<T>(t) }
    {
    }
};

Всегда ли это так, что конструктор foo1 представляет правильный способ инициализации переменной-члена T? т.е. используя std::move,

Всегда ли это так, что конструктор foo2 представляет правильный способ инициализации переменной-члена foo1<T> из-за необходимости пересылки в конструктор foo1? т.е. используя std::forward,

Обновить

Следующий пример не работает для foo1 с помощью std::move:

template <typename T>
foo1<T> make_foo1(T&& t)
{
    return{ std::forward<T>(t) };
}

struct bah {};

int main()
{
    bah b;

    make_foo1(b); // compiler error as std::move cannot be used on reference

    return EXIT_SUCCESS;
}

Что является проблемой, так как я хочу, чтобы T был и ссылочным типом, и типом значения.

2 ответа

Решение

Ни один из этих примеров не использует универсальные ссылки (переадресация ссылок, как они теперь называются).

Пересылочные ссылки формируются только при наличии дедукции типа, но T&& в конструкторах для foo1 а также foo2 не выводится, так что это просто ссылка.

Так как оба являются rvalue ссылками, вы должны использовать std::move на обоих.

Если вы хотите использовать переадресацию ссылок, вы должны сделать так, чтобы конструкторы имели выведенный аргумент шаблона:

template <typename T>
struct foo1
{
    T t_;

    template <typename U>
    foo1(U&& u) :
        t_{ std::forward<U>(u) }
    {
    }
};

template <typename T>
struct foo2
{
    foo1<T> t_;

    template <typename U>
    foo2(U&& u) :
        t_{ std::forward<U>(u) }
    {
    }
};

Вы не должны использовать std::move в foo1 в этом случае, поскольку клиентский код может передать lvalue и сделать объект недействительным в молчании:

std::vector<int> v {0,1,2};
foo1<std::vector<int>> foo = v;
std::cout << v[2]; //yay, undefined behaviour

Более простой подход заключается в том, чтобы принимать по стоимости и безоговорочно std::move в хранилище:

template <typename T>
struct foo1
{
    T t_;

    foo1(T t) :
        t_{ std::move(t) }
    {
    }
};

template <typename T>
struct foo2
{
    foo1<T> t_;

    foo2(T t) :
        t_{ std::move(t) }
    {
    }
};

Для идеальной версии пересылки:

  • Прошло lvalue -> один экземпляр
  • Прошло rvalue -> один ход

Для передачи по значению и версии перемещения:

  • Прошло lvalue -> одна копия, один ход
  • Прошло rvalue -> два хода

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

Это зависит от того, как вы выводите T, Например:

template<class T>
foo1<T> make_foo1( T&& t ) {
  return std::forward<T>(t);
}

В этом случае T в foo1<T> ссылка для пересылки, и ваш код не будет компилироваться.

std::vector<int> bob{1,2,3};
auto foo = make_foo1(bob);

вышеприведенный код молча перенесен из bob в std::vector<int>& в конструкторе foo1<std::vector<int>&>,

Делать то же самое с foo2 должно сработать. Вы бы получили foo2<std::vector<int>&>и будет содержать ссылку на bob,

Когда вы пишете шаблон, вы должны рассмотреть, что это значит для типа T быть ссылкой. Если ваш код не поддерживает ссылки, рассмотрите static_assert или SFINAE, чтобы заблокировать это дело.

template <typename T>
struct foo1 {
  static_assert(!std::is_reference<T>{});
  T t_;

  foo1(T&& t) :
    t_{ std::move(t) }
  {
  }
};

Теперь этот код генерирует разумное сообщение об ошибке.

Вы можете подумать, что существующее сообщение об ошибке было в порядке, но оно было только в порядке, потому что мы перешли в T,

template <typename T>
struct foo1 {
  static_assert(!std::is_reference<T>{});

  foo1(T&& t)
  {
    auto internal_t = std::move(t);
  }
};

здесь только static_assert гарантирует, что наши T&& был фактическим значением.


Но хватит с этим теоретическим списком проблем. У вас есть конкретный.

В конце концов, это, вероятно, хотите, что вы хотите:

template <class T> // typename is too many letters
struct foo1 {
  static_assert(!std::is_reference<T>{});
  T t_;

  template<class U,
    class dU=std::decay_t<U>, // or remove ref and cv
    // SFINAE guard required for all reasonable 1-argument forwarding
    // reference constructors:
    std::enable_if_t<
      !std::is_same<dU, foo1>{} && // does not apply to `foo1` itself
      std::is_convertible<U, T> // fail early, instead of in body
    ,int> = 0
  >
  foo1(U&& u):
    t_(std::forward<U>(u))
  {}
  // explicitly default special member functions:
  foo1()=default;
  foo1(foo1 const&)=default;
  foo1(foo1 &&)=default;
  foo1& operator=(foo1 const&)=default;
  foo1& operator=(foo1 &&)=default;
};

или более простой случай, который так же хорош в случаях 99/100:

template <class T>
struct foo1 {
  static_assert(!std::is_reference<T>{});
  T t_;

  foo1(T t) :
    t_{ std::move(t) }
  {}
  // default special member functions, just because I never ever
  // want to have to memorize the rules that makes them not exist
  // or exist based on what other code I have written:
  foo1()=default;
  foo1(foo1 const&)=default;
  foo1(foo1 &&)=default;
  foo1& operator=(foo1 const&)=default;
  foo1& operator=(foo1 &&)=default;
};

Как правило, этот более простой метод дает ровно на 1 ход больше, чем идеальный метод пересылки, в обмен на огромное количество кода и сложности. И это позволяет {} инициализация T t аргумент вашего конструктора, что приятно.

Другие вопросы по тегам