#опытным
Увидел на ревью интересный кейс. Мы о нем уже говорили, что не сильно акцентировали внимание. Сегодня больше времени уделим одному интересному явлению.
Если у вас есть какой-то шаблонный класс, который хранит тип Т, и в этом классе есть сеттер на этот тип, то по давней привычке(еще с 98 стандарта) его можно написать вот так:
template <class T>
struct TemplateClass {
void SetValue(const T& value) {
value_ = value;
}
private:
T value_;
};
Привычка - дело хорошее и экономит ресурс мозга на выполнение действий. Не так много когнитивного внимания нужно тратить на деятельность.
Но иногда привычки ограничивают нас. Мы-то уже в modern C++ эре. И в данном случае как раз такой кейс.
Что будет, если мы захотим передать в этот метод временный объект? Например так:
struct ShowConstruct {
ShowConstruct() = default;
ShowConstruct(int value) : field{value} {
std::cout << "Param construct " << field << std::endl;}
ShowConstruct& operator=(const ShowConstruct& other) {
field = other.field;
std::cout << "Copy assign " << field << std::endl;
return *this;}
ShowConstruct& operator=(ShowConstruct&& other) {
field = other.field;
std::cout << "Move assign " << field << std::endl;
return *this;}
int field = 0;
};
TemplateClass<ShowConstruct> obj;
obj.SetValue(ShowConstruct{5});На экран выведется:
Param construct 5
Copy assign 5
Это значит, что даже если мы передаем в такой сеттер временный объект, у которого можно забрать его ресурсы и сэкономить на копировании, мы все равно не получаем этих бенефитов.
Потому что в сеттере value уже относится к категории lvalue. А при присваивании объекта от lvalue будет вызываться копирующий оператор присваивания.
А нам бы хотелось, чтобы вызывался перемещающий оператор. Как этого достичь?
Использовать универсальную ссылку. Скажет прошаренный читатель.
Для шаблонного кода мы можем пометить параметр метода двумя амперсандами и дальше внутри передавать его во все места через std::forward. Таким образом, если нам на вход пришел именованный объект, то std::forward скастует его к lvalue ссылке, а если временный, то к rvalue ссылке. И это поможет нам в нужных случая вызывать правильный оператор присваивания. И std::forward и universal reference доступны с 11-го стандарта вместе с введением мув-семантики.
template <class T>
struct TemplateClass {
void SetValue(T&& value) {
value_ = std::forward<T>(value);
}
private:
T value_;
};
TemplateClass<ShowConstruct> obj;
obj.SetValue(ShowConstruct{5});
Теперь мы получаем нужный вывод:
Param construct 5
Move assign 5
Однако этот прошаренный читатель оказался не таким уж и прошаренным! Такая штука не сработает для шаблонных параметров класса!
in class template argument deduction, template parameter
of a class template is never a forwarding reference
Универсальная ссылка(она же forwarding reference) появляется только, когда тип параметра функции Т&& и Т - шаблонной параметр самой функции. В нашем случае нет никакого вывода - тип Т известен из класса. Поэтому и никакой универсальной ссылки не появляется.
Мы просто определили метод, который принимает rvalue ссылку. При попытке передать туда lvalue будет ошибка:
TemplateClass<ShowConstruct> obj;
ShowConstruct lvalue{7};
obj.SetValue(lvalue);
//ERROR: rvalue reference to type 'ShowConstruct'
// cannot bind to lvalue of type 'ShowConstruct'
Какой выход? Просто рядышком с сеттером для константной lvalue ссылки написать сеттер для rvalue ссылки.
template <class T>
struct TemplateClass {
void SetValue(const T& value) {
value_ = value;
}
void SetValue(T&& value) {
value_ = std::move(value);
}
private:
T value_;
};
TemplateClass<ShowConstruct> obj;
obj.SetValue(ShowConstruct{5});
ShowConstruct lvalue{7};
obj.SetValue(lvalue);
Тогда все нормально скомпилируется и в нужных места будут вызваны нужные операторы.
Stay universal. Stay cool.
#cpp11