Что означает T&& (двойной амперсанд) в C++11?
Что такое T&& и как это работает?
Двойной амперсанд && в C++11 — это rvalue-ссылка (rvalue reference). Она отличается от обычной lvalue-ссылки (&) тем, что позволяет привязываться к временным объектам (rvalue) и, главное, изменять их. Это ключ к семантике перемещения (move semantics) и идеальной передаче (perfect forwarding).
1. Основы: lvalue vs rvalue
Напомню:
- lvalue — объект, у которого есть имя и адрес (например, переменная
int x = 5;). - rvalue — временное значение без имени (например,
5, результат функцииgetTemp()).
Обычная ссылка int& может привязываться только к lvalue. Rvalue-ссылка int&& — только к rvalue:
int x = 10;
int& lref = x; // OK
int& lref2 = 10; // Ошибка! 10 — rvalue
int&& rref = 10; // OK
int&& rref2 = x; // Ошибка! x — lvalue
2. Специальный случай: T&& в шаблонах (универсальная ссылка)
Когда вы пишете template<typename T> void foo(T&& arg), это не всегда rvalue-ссылка. Это универсальная ссылка (forwarding reference). Её тип выводится компилятором в зависимости от того, что вы передаёте:
- Если передаёте lvalue (например,
int x; foo(x);), тоTвыводится какint&, иT&&становитсяint& &&, что коллапсирует вint&(lvalue-ссылка). - Если передаёте rvalue (например,
foo(5);), тоTвыводится какint, иT&&становитсяint&&(rvalue-ссылка).
Это правило называется сворачиванием ссылок (reference collapsing). Оно работает только в шаблонах (или с auto&&).
3. Зачем это нужно? Семантика перемещения
Rvalue-ссылки позволяют реализовать move-конструкторы и move-операторы присваивания. Вместо глубокого копирования вы «воруете» ресурсы у временного объекта:
class Buffer {
int* data;
size_t size;
public:
// Конструктор копирования (дорого)
Buffer(const Buffer& other) : size(other.size) {
data = new int[size];
std::copy(other.data, other.data + size, data);
}
// Move-конструктор (дёшево)
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // Оставляем источник в пустом состоянии
other.size = 0;
}
};
Теперь Buffer b = createBuffer(); вызовет move-конструктор, а не копирование.
4. std::move и std::forward
std::move— просто приводит lvalue к rvalue (выглядит какstatic_cast<T&&>(arg)). Используйте, когда хотите безусловно переместить объект.std::forward— условное приведение: сохраняет «категорию» значения (lvalue остаётся lvalue, rvalue — rvalue). Используется в шаблонах для идеальной передачи.
Пример идеальной передачи:
template<typename T>
void wrapper(T&& arg) {
// Передаём arg в другую функцию с сохранением его категории
someFunction(std::forward<T>(arg));
}
Без std::forward arg всегда будет lvalue (у него есть имя), и вы потеряете возможность перемещения.
Итог
Type&&вне шаблона — rvalue-ссылка (только для временных объектов).T&&в шаблоне — универсальная ссылка (может быть lvalue или rvalue).- Используйте
std::moveдля безусловного перемещения,std::forward— для условного в шаблонах.
Дополнение: важные детали для новичков
Добавлю пару моментов, которые часто вызывают путаницу:
1. Rvalue-ссылки не всегда ведут к перемещению
Просто объявить параметр как MyClass&& недостаточно — нужно внутри функции вызвать std::move, чтобы активировать семантику перемещения:
void process(MyClass&& obj) {
// obj — это lvalue (у него есть имя!)
// Чтобы переместить, нужно:
MyClass moved = std::move(obj);
}
2. Правило пяти
Если вы определили move-конструктор, скорее всего, понадобятся и остальные специальные функции-члены: деструктор, копирующий конструктор, копирующее присваивание, перемещающее присваивание. Или используйте = default.
3. Производительность
Не злоупотребляйте std::move — он не делает код магически быстрее. Перемещение имеет смысл для объектов, владеющих ресурсами (память, файлы, сокеты). Для простых типов (int, double) копирование и перемещение — одно и то же.
Совет: начните с понимания lvalue/rvalue, потом переходите к универсальным ссылкам. И не бойтесь экспериментировать с простыми примерами в компиляторе!