Что означает T&& (двойной амперсанд) в C++11?
Краткий ответ: Rvalue-ссылки и семантика перемещения
T&& в C++11 — это rvalue-ссылка (ссылка на временный объект). Она позволяет различать lvalue (объекты, у которых есть имя и адрес) и rvalue (временные объекты, которые вот-вот будут уничтожены). Это ключевое нововведение для семантики перемещения и идеальной передачи.
Почему это нужно?
До C++11 мы могли передавать объекты только по значению (копирование) или по ссылке (lvalue-ссылка). Проблема: при возврате временного объекта из функции или передаче его в другую функцию происходило дорогое копирование, даже если оригинал уже не нужен. Rvalue-ссылки позволяют "перехватить" временный объект и переместить его ресурсы (например, динамическую память) без копирования.
Пример: класс строки
class MyString {
char* data;
size_t size;
public:
// Обычный конструктор (копирование)
MyString(const MyString& other) {
size = other.size;
data = new char[size];
memcpy(data, other.data, size); // Дорогое копирование
}
// Конструктор перемещения (C++11)
MyString(MyString&& other) noexcept {
data = other.data; // Просто копируем указатель
size = other.size;
other.data = nullptr; // Обнуляем источник
// Никакого выделения памяти!
}
};
Когда вы пишете MyString c = createString(); — createString() возвращает временный объект (rvalue). Компилятор вызовет конструктор перемещения, а не копирования. Это намного быстрее.
Правила сворачивания ссылок (reference collapsing)
В шаблонах с T&& есть хитрость. Когда вы пишете:
template<typename T>
void foo(T&& arg);
Тип T&& ведет себя не как rvalue-ссылка, а как универсальная ссылка (forwarding reference), если T выводится компилятором. Правила сворачивания:
T& &->T&T& &&->T&T&& &->T&T&& &&->T&&
То есть если вы передаете lvalue в foo, T выводится как X&, и по правилам сворачивания X& && становится X& — обычная lvalue-ссылка. Если передаете rvalue, T выводится как X, и получается X&&.
Идеальная передача (perfect forwarding)
Это позволяет сохранить категорию значения при передаче аргумента другой функции:
template<typename T>
void wrapper(T&& arg) {
// arg может быть lvalue или rvalue
// Нам нужно передать его дальше с сохранением категории
target(std::forward<T>(arg)); // std::forward — это conditional move
}
void target(MyString& s) { /* lvalue */ }
void target(MyString&& s) { /* rvalue */ }
std::forward смотрит на тип T: если T — lvalue-ссылка, то arg остается lvalue; если T — не ссылка, то arg приводится к rvalue.
Когда использовать?
- Конструкторы перемещения и операторы присваивания:
MyString(MyString&&) - Функции, которые принимают временные объекты:
void push_back(T&& value) - Шаблоны с универсальными ссылками для perfect forwarding
- Не используйте
T&&в обычных функциях (не шаблонах), если не уверены — это будет только rvalue-ссылка, и вы не сможете передать lvalue.
Важное предостережение
Не путайте T&& с const T&. const T& принимает и lvalue, и rvalue, но не позволяет изменять объект. T&& принимает только rvalue (кроме случаев с выводом шаблона) и позволяет изменять и перемещать ресурсы.
Надеюсь, теперь стало понятнее! Rvalue-ссылки — одна из самых мощных фич C++11, которая делает код быстрее без потери удобства.
<<Альтернативный взгляд: T&& как инструмент оптимизации
Если вы не пишете библиотеки общего назначения, а просто используете стандартную библиотеку, то T&& чаще всего встречается в контексте std::move и std::forward. Вот практический совет:
- Когда видите функцию, принимающую
std::vector<int>&&— это значит, что функция ожидает временный вектор и может "украсть" его данные. - Если у вас есть lvalue (именованный объект), который больше не нужен, оберните его в
std::move:process(std::move(myVector)); - Не злоупотребляйте
std::move— после перемещения объект находится в "valid but unspecified state" (корректном, но неопределенном состоянии). Обычно его можно только уничтожить или присвоить новое значение.
Вот типичный пример из реального кода:
class DataProcessor {
std::vector<int> data;
public:
// Принимаем rvalue — перемещаем данные внутрь
void setData(std::vector<int>&& newData) {
data = std::move(newData); // Избегаем копирования
}
// Принимаем lvalue — копируем
void setData(const std::vector<int>& newData) {
data = newData; // Копирование
}
};
// Использование:
DataProcessor dp;
std::vector<int> v = {1, 2, 3};
dp.setData(v); // Копирование (v остался)
dp.setData(std::move(v)); // Перемещение (v теперь пуст)
dp.setData({4, 5, 6}); // Перемещение (временный объект)
Главное правило: если вы не уверены, используйте const T& — это безопасно для чтения. T&& используйте только когда точно нужно перемещать ресурсы и вы понимаете время жизни объектов.