Привет, Хабр! Представляю вашему вниманию перевод статьи Eli Bendersky, Understanding of lvalues and rvalues in C and C++.
От переводчика: предлагаю Вашему вниманию перевод интересной статьи об lvalue и rvalue в языках C/C++. Тема не нова, но знать об этих понятиях никогда не поздно. Статья рассчитана на новичков, либо на программистов переходящих с C (или других языков) на C++. Поэтому будьте готовы к подробному разжёвыванию. Если вам интересно, добро пожаловать под кат
Термины lvalue и rvalue не являются чем-то таким, с чем часто приходится сталкиваться при программировании на C/C++, а при встрече не сразу становится ясным, что именно они означают. Наиболее вероятное место столкнуться с ними — это сообщения компилятора. Например, при компиляции следующего кода компилятором gcc
:
int foo() { return 2; }
int main()
{
foo() = 2;
return 0;
}
test.c: In function 'main':
test.c:8:5: error: lvalue required as left operand of assignment
g++
:int& foo()
{
return 2;
}
testcpp.cpp: In function 'int& foo()':
testcpp.cpp:5:12: error: invalid initialization of non-const reference
of type 'int&' from an rvalue of type 'int'
int var;
var = 4;
var
является lvalue, потому что это объект с идентифицируемым местом в памяти. С другой стороны, следующие заклинания приведут к ошибкам:4 = var; // ERROR!
(var + 1) = 4; // ERROR!
4
, ни выражение var + 1
не являются lvaluefoo
возвращает временное значение, которое является rvalue. Попытка присваивания является ошибкой. То есть, видя код foo() = 2;
, компилятор сообщает, что ожидает lvalue с левой стороны оператора присваивания.int globalvar = 20;
int& foo()
{
return globalvar;
}
int main()
{
foo() = 10;
return 0;
}
foo
возвращает ссылку, которая является lvalue, то есть ей можно придать значение. Вообще, в C++ возможность возвращать lvalue, как результат вызова функции, существенна для реализации некоторых перегруженных операторов. Как пример приведём перегрузку оператора []
в классах, которые реализуют доступ по результатам поиска. Например std::map
:std::map<int, float> mymap;
mymap[10] = 5.6;
mymap[10]
работает, потому что неконстантная перегрузка std::map::operator[]
возвращает ссылку, которой может быть присвоено значение.const
, это определение нужно было доработать. Действительно:const int a = 10; // 'a' - lvalue
a = 10; // но ему не может быть присвоено значение!
[...] lvalue, тип которого не является массивом, не является неполным, не имеет спецификаторconst
, не является структурой или объединением, содержащими поля (также включая поля, рекурсивно вложенные в содержащиеся агрегаты и объединения) со спецификаторомconst
.
int a = 1; // a - lvalue
int b = 2; // b - lvalue
int c = a + b; // '+' требует rvalue, поэтому a и b конвертируются в rvalue
// и rvalue возвращается в качестве результата
a
и b
оба lvalue. Поэтому в третьей строке они подвергаются неявному преобразованию lvalue-в-rvalue. Все lvalue, которые не являются массивом, функцией и не имеют неполный тип, могут быть преобразованы в rvalue.int arr[] = {1, 2};
int* p = &arr[0];
*(p + 1) = 10; // OK: p + 1 rvalue, однако *(p + 1) уже lvalue
int var = 10;
int* bad_addr = &(var + 1); // ОШИБКА: требуется lvalue для унарного оператора '&'
int* addr = &var; // ОК: var - lvalue
&var = 40; // ОШИБКА: требуется lvalue с левой стороны
// оператора присваивания
std::string& sref = std::string(); // ОШИБКА: неверная инициализация
// неконстантной ссылки типа 'std::string&'
// rvalue типа 'std::string'
lvalue (3.10) на тип T, не являющимся функциональным, или массивом, может быть преобразован в rvalue. [...] Если T не класс, типом rvalue является cv-неспецифицированная версия типа T. Иначе, типом rvalue является T.Так что же значит «cv-неспецифицированный»? CV-спецификатор — это термин, используемый для описания const и volatile спецификаторов типа.
Каждый тип, который является cv-неспецифицированным полным или неполным объектным типом или типом void (3.9), имеет соответственно три cv-специфицированные версии: тип со спецификатором const, тип со спецификатором volatile и тип со спецификаторами const volatile. [...] CV-специфицированные и cv-неспецифицированные типы являются различными, однако они имеют одинаковое представление и требования по выравниванию.Но как всё это связано с rvalue? В языке C rvalue никогда не имеют cv-специфицированных типов. Это свойство lvalue. Однако в C++ классовые rvalue могут быть cv-специфицированным, что не касается встроенных типов вроде
int
. Рассмотрим пример:#include <iostream>
class A {
public:
void foo() const { std::cout << "A::foo() const\n"; }
void foo() { std::cout << "A::foo()\n"; }
};
A bar() { return A(); }
const A cbar() { return A(); }
int main()
{
bar().foo(); // вызовет foo
cbar().foo(); // вызовет foo const
}
main
вызовет метод foo() const
, так как cbar
возвращает объект типа const A
, который отличен от A
. Это как раз то, что имелось в виду в последнем предложении выдержки из стандарта выше. Кстати, заметьте, что возвращаемое cbar
значение является rvalue. Это был пример cv-специфицированных rvalue в действии. class Intvec
{
public:
explicit Intvec(size_t num = 0)
: m_size(num), m_data(new int[m_size])
{
log("constructor");
}
~Intvec()
{
log("destructor");
if (m_data) {
delete[] m_data;
m_data = 0;
}
}
Intvec(const Intvec& other)
: m_size(other.m_size), m_data(new int[m_size])
{
log("copy constructor");
for (size_t i = 0; i < m_size; ++i)
m_data[i] = other.m_data[i];
}
Intvec& operator=(const Intvec& other)
{
log("copy assignment operator");
Intvec tmp(other);
std::swap(m_size, tmp.m_size);
std::swap(m_data, tmp.m_data);
return *this;
}
private:
void log(const char* msg)
{
cout << "[" << this << "] " << msg << "\n";
}
size_t m_size;
int* m_data;
};
std::swap
, мы можем быть уверены, что не может возникнуть промежуточного состояния с непроинициализированной памятью, если где-то произойдёт исключение). Все они используют функцию логирования, чтобы мы могли понять, когда они на самом деле вызваны.v1
в v2
:Intvec v1(20);
Intvec v2;
cout << "assigning lvalue...\n";
v2 = v1;
cout << "ended assigning lvalue...\n";
assigning lvalue...
[0x28fef8] copy assignment operator
[0x28fec8] copy constructor
[0x28fec8] destructor
ended assigning lvalue...
v2
некоторое rvalue:cout << "assigning rvalue...\n";
v2 = Intvec(33);
cout << "ended assigning rvalue...\n";
v2
(это может случится например, если функция возвращает вектор). Вот что мы увидим на экране:assigning rvalue...
[0x28ff08] constructor
[0x28fef8] copy assignment operator
[0x28fec8] copy constructor
[0x28fec8] destructor
[0x28ff08] destructor
ended assigning rvalue...
operator=
копирующим оператором присваивания. В C++11 эта разница становится важной). Давайте добавим другой operator=
в IntVec
:Intvec& operator=(Intvec&& other)
{
log("move assignment operator");
std::swap(m_size, other.m_size);
std::swap(m_data, other.m_data);
return *this;
}
assigning rvalue...
[0x28ff08] constructor
[0x28fef8] move assignment operator
[0x28ff08] destructor
ended assigning rvalue...
v2
. Вызовы конструктора и деструктора всё же необходимы для временного объекта, который создаётся через Intvec(33)
. Однако другой временный объект внутри оператора присваивания больше не нужен. Оператор просто меняет внутренний буфер rvalue со своим, и таким образом деструктор rvalue удаляет буфер самого объекта, который больше не будет использоваться. Чисто!К сожалению, не доступен сервер mySQL