Раскрытие тайн C ++ lvalues и rvalues (II)
Часть вторая: ссылки на lvalues и rvalues.
С появлением C ++ 11 значение и важность lvalues и rvalues расширились. Это важные понятия, которые нужно понять, поскольку они используются по-разному, и вы обнаружите, что они связаны со многими концепциями, такими как ссылки lvalue / rvalue, семантика перемещения, идеальная пересылка и конструкторы перемещения среди других. В этой серии из трех частей мы собираемся раскрыть все эти элементы, чтобы предоставить вам понятные и полезные знания для ваших будущих проектов C ++.
В прошлой статье мы изучили основы lvalues и rvalues и изучили, как они работают вместе с функциями и объектами. Теперь у нас есть основы для понимания ссылок lvalue и rvalue - пары концепций, представленных в C ++ 11.
Во-первых, мы собираемся наблюдать, что происходит со ссылками lvalue. Рассмотрим следующий код:
class AnObject { //… public: AnObject(){} AnObject(int param){} }; AnObject getAnObject() { return AnObject(); } int main () { // Lvalue refs AnObject Object; AnObject &refToObject = Object; AnObject &refToObject2 = getAnObject(); const AnObject &refToObject3 = getAnObject(); AnObject Object2(AnObject()); }
Ссылки на объекты работают нормально, потому что ‘Object’ - это lvalue, и вы создаете альтернативное имя этого объекта с помощью ‘refToObject’:
//This works fine AnObject &refToObject = Object;
И, как вы, вероятно, ожидали, создание псевдонима (lvalue ссылка на объект) путем присвоения ему временного rvalue a вообще не сработает:
//This line will not compile AnObject &refToObject2 = getAnObject();
Но есть обходной путь. Помните: когда вы создаете ссылку, вы даете другое имя тому же объекту, поэтому любое изменение, внесенное в одно из имен объекта, всегда влияет на другое. Следовательно, делая нашу ссылочную константу постоянной, время жизни rvalue увеличивается, потому что псевдоним, который мы создаем, не может быть изменен.
//This line will compile const AnObject &refToObject3 = getAnObject();
Любопытно, что можно даже выполнить какую-то вложенную инициализацию объекта, используя во внутреннем объекте тот же класс, что и rvalue, например:
//This line may or may not compile depending on the compiler you use AnObject Object2(AnObject());
Однако вы получите смешанные результаты в зависимости от используемого вами компилятора, причем некоторые из них не позволят вам скомпилировать эту строку. Некоторые компиляторы могут быть сбиты с толку из-за внутренних скобок, поэтому вы можете закончить объявление функции с именем Object2 вместо создания объекта. Даже если код компилируется и круглые скобки считаются объявлением функции, программа может привести к неожиданным результатам или даже к сбою.
Одним из распространенных решений является использование дополнительных круглых скобок, поэтому невозможно проанализировать выражение как объявление функции, например:
//This line will compile AnObject Object2((AnObject()));
Другое решение - использовать параметризованный конструктор вместо конструктора по умолчанию, чтобы не было двусмысленности:
//This line will compile with a parameterized constructor AnObject Object2(AnObject(1));
Но начиная с C ++ 11 существует единообразная инициализация, которая обеспечивает очень элегантное решение:
//This line will compile using uniform initialization AnObject Object2(AnObject{});
Итак, теперь мы рассмотрели основы ссылок на lvalue. Давайте посмотрим, о чем идет речь в ссылке rvalue. Для начала мы можем сделать следующий код действительным, используя ссылку rvalue:
// As you remember, this will not compile AnObject &refToObject2 = getAnObject();
Нам просто нужно изменить ссылку на оператор ссылки rvalue ‘&&’, и теперь наш код действителен:
// And now this will compile AnObject &&refToObject2 = getAnObject();
Конечно, теперь мы также можем скомпилировать строку, если мы свяжемся с конструктором объекта.
// This also will compile AnObject &&refToObject2 = AnObject();
Ссылки Rvalues также полезны для перегрузки функций. Определим две версии функции:
//Lvalue test void test(AnObject &testObject) { std::cout << "testObject is a lvalue" << std::endl; } //Rvalue test void test(AnObject &&testObject) { std::cout << "testObject is a rvalue" << std::endl; }
Каждая версия будет вызываться в зависимости от типа объекта, используемого в качестве аргумента:
AnObject testObject; test(testObject); //Output: testObject is a lvalue test(getAnObject()); //Output: testObject is a rvalue test(AnObject()); //Output: testObject is a rvalue
Теперь давайте попробуем что-нибудь другое и создадим две функции с целым числом в качестве аргумента:
void testInteger(int &i) { std::cout << “lvalue reference “<< i << std::endl; } void testInteger(int &&i) { std::cout << “rvalue reference “<< i << std::endl; } int main () { int x = 10; testInteger(x); testInteger(5); return 0; }
Как и ожидалось, первый вызов вызовет версию lvalue:
int x = 10; testInteger(x); //Output: lvalue reference 10
Кроме того, второй вызов ведет себя должным образом, вызывая версию rvalue:
testInteger(5); //Output: rvalue reference 5
Но что произойдет, если мы добавим третью версию функции testInteger () с целым числом в качестве аргумента? Мы получим ошибку, потому что вызов перегруженной функции неоднозначен, и компилятор не сможет провести различие между этой функцией и testInteger (int & i) (lvalue версия):
//This will not compile void testInteger(int i) { std::cout << "No reference "<< i << std::endl; }
Еще одно наблюдение - это то, как операторы приращения префикса и постфикса взаимодействуют с нашими перегруженными функциями. Если мы вызовем функцию с постфиксным оператором, будет вызвана версия rvalue, поскольку временное значение будет создано до приращения x:
testInteger(x++); //Output: rvalue reference 10 std::cout << “x value “<< x << std::endl; //Output: x value 11
С другой стороны, если мы вызовем функцию с префиксным оператором, будет вызвана версия lvalue, поскольку x будет увеличиваться перед передачей в качестве аргумента:
testInteger(++x); //Output: lvalue reference 12 std::cout << "x value "<< x << std::endl; //Output: x value 12
Все приведенные выше примеры довольно легко понять, не так ли? Однако понимание разницы между ссылками lvalue и rvalue имеет решающее значение для темы следующей части статьи. Оказывается, rvalues используются для повышения эффективности кода при создании объектов. А пока желаю удачного кодирования!