Std:: move или std:: forward при назначении универсального конструктора переменной-члену в С++
Рассмотрим следующие классы 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 являлся как ссылочным типом, так и типом значения.
Ответы
Ответ 1
Ни один из этих примеров не использует универсальные ссылки (пересылка ссылок, как они теперь называются).
Ссылки на пересылку создаются только при наличии вывода типа, но T&&
в конструкторах для foo1
и foo2
не выводится, поэтому это просто ссылка rvalue.
Поскольку обе ссылки 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 → два хода
Подумайте, как должен выглядеть этот код и сколько его нужно будет изменить и сохранить, и выберите вариант, основанный на этом.
Ответ 2
Это зависит от того, как вы выберете 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&&
был фактическим значением r.
Но достаточно с этим теоретическим списком проблем. У вас есть конкретный вариант.
В конце концов, вероятно, вы хотите:
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
вашему конструктору, что приятно.