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
аргумент вашего конструктора, что приятно.