Раскрытие тайн 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 ​​ используются для повышения эффективности кода при создании объектов. А пока желаю удачного кодирования!