Академический Документы
Профессиональный Документы
Культура Документы
Содержание:
int a, b;
a = 3; // Корректно
b = a; // Корректно
3 = b; // Ошибка
Исходя из этого простого примера можно сделать вывод, что нельзя просто так
взять и присвоить 3 какое-то новое значение. Хотя, казалось бы, это должно
быть очень веселым занятием Напрашивается вопрос, можно ли как-то
классифицировать выражения по действиям над ними? Существуют ли еще
какие-то особые правила?
lvalue
lvalue
a = 3;
rvalue
prvalue
prvalue
a = 3;
xvalue
lvalue rvalue
a = 3;
rvalue rvalue
(a + b) = a // Ошибка!
lvalue rvalue
*(pointer + 1) = a // Ок
Однако, это ни разу не преобразование! Мы знаем, что pointer указывает на
область памяти, в которой лежит какой-то объект. К нему мы имеем доступ и
можем изменять. Поэтому оператор разыменования указателя возвращает
lvalue .
lvalue rvalue
pointer[1] = a // Аналогично, ок
array[3] = 4; // Ок
3[array] = 5; // Ок
Опять же, никакой магии По сути это влияет лишь на порядок "слагаемых":
*(array + 3) = 4;
*(3 + array) = 5;
value = foo();
~~^~~ ~~^~~
lvalue rvalue
foo() = 42;
~~^~~ ^~~~~
lvalue rvalue
*(&some_value) = 42;
~~~~~~~^~~~~~~ ^~~
rvalue -> lvalue rvalue
cpp
int& foo() { return some_value; }
value = foo();
~~^~~ ~~^~~
lvalue rvalue
value = *(&some_value);
~~^~~ ~~~~~~~^~~~~~~~
lvalue rvalue -> lvalue -> rvalue
Что еще можно вытащить из знания rvalue и lvalue ? В свое время был
популярен Yoda-style при сравнении переменных. Смысл его в том, чтобы
rvalue и константы всегда находились слева от оператора == . Так, в случае
опечатки, программист мог написать оператор присвоения = , но компилятор
сообщил бы ошибку. Не скажу, что сейчас это популярная практика, есть все
таки предупреждения компилятора.
Про const вы, наверняка, уже знаете. Вот о квалификаторе volatile мы еще
не говорили, от нас тут нужна хорошая подводка... В рамках этой темы
достаточно знать, что volatile переменные всегда должны лежать в
оперативной памяти (т.н. запрет на кеширование значений; запрет на
оптимизацию) или привязаны к области ввода-вывода.
Несмотря на то, что переменной magic нельзя присвоить новое значение, она
всё ещё принадлежит категории lvalue :
// lvalue rvalue
magic = 5;
// ~~^~~
// Error: assignment of
// read-only variable 'magic'
Нельзя сказать, что неизменяемый тип является rvalue. Нет, это просто другое
свойство, которое накладывает ограничения на действия над данными. Однако,
такие выражения могут быть использованы только как rvalue . Т.е. могут быть
только прочитаны, скопированы. Это позволяет ослабить ограничения в таких
ситуациях:
int a = 1; // Ok
int &b = a; // Ok
int &c = 2; // Error!
class string
{
public:
// Constructor for
// rvalue reference of string 'other'
string(string &&other) noexcept
{ ... }
Теперь каждый класс может определять внутри себя логику передачи владения
ресурсом. Таким образом, получилось интегрировать нововведения в
действующую языковую модель.
Будь я на вашем месте, мне бы стало непонятно, как же использовать всю эту
лабуду? Есть специальная функция std::move :
Её вызов позволяет перейти к категории выражения xvalue . Все что она делает
— это вот:
// Упрощенная!!! версия
template<class T>
T&& move(T& t)
{
return static_cast<T&&>(t);
}
Дословно, эта функция преобразует тип данных T& — lvalue к T&& — rvalue
reference . Шаблон позволяет её определять для разных типов, а CTAD не
указывать конкретный тип. Получается достаточно приятный глазу код :)
Итак, std::move вернет в правой части rvalue reference , а для левой будет
вызван специальный перемещающий конструктор:
// Псевдокод!
string(string &&other) noexcept
{
this->data = other.data;
this->size = other.size;
other.data = nullptr;
other.size = 0;
}
Вы наверняка слышали про т.н. правило трех. Так вот теперь с нововведением
это правило расширяется до пяти. Конечно, необходимость их определения
вытекает из использования этой std::move семантики.
template<typename T>
void foo(T &&message)
{
...
}
Ожидается, что из него будет выведен тип rvalue reference , но это не всегда
так. Такие ссылки позволяют с одной стороны определить поведения для
работы с xvalue , а с другой, неожиданно, для lvalue .
// Передает lvalue
foo(str);
template<typename T>
void f(std::vector<T> &¶m);
// ~~^~~
// rvalue reference
template<typename T>
void f(T &¶m);
// ~~^~~
// universal reference
template<class T>
class mycontainer
{
public:
void push_back(T &&other) { ... }
~~~^~~~
rvalue reference
...
};
Пример: https://compiler-explorer.com/z/We4qzG5xG
template<class... Ts>
void foo(Ts... args)
{
bar(args...);
}
foo(std::move(string), value);
~~~~^~~~ ~~^~~~
xvalue lvalue
int main()
{
MyStruct ms;
foo(ms); //calls foo(T&&)
return 0;
}
template<typename T>
void foo(T &&message)
{
T tmp(std::forward<T>(message));
...
}
cpp
std::string str = "blah blah blah";
template<class T>
class wrapper
{
std::vector<T> m_data;
public:
template<class Y>
wrapper(Y &&data)
: m_data(std::forward<Y>(data))
{
// make a copy from `data` or move resources from `data`
}
};
class string
{
public:
string(string &&other) noexcept
{ ... }
};
И неспроста я это делал! Я бы даже сказал, что где-то это является очень
важным требованием.
std::move_if_noexcept(object);
struct apple
{
std::vector<seed> seeds;
}
struct apple_bucket
{
std::vector<apple> bucket
}
struct truck_of_apple_buckets
{
std::vector<apple_bucket> buckets;
}
apple_bucket
std::string get_very_long_string();
Эта мысль волновала всех с давних времен... Поэтому она нашла поддержку от
разработчиков компиляторов.
// RVO example
Foo f()
{
return Foo();
}
// NRVO example
Foo f()
{
Foo named_object;
return named_object;
}
// Foo no coping
Foo obj = f();
int main()
{
auto *address = reinterpret_cast<Foo *>(
// allocate memory directly on stack!
alloca(sizeof(Foo))
);
f(address);
}
// RVO example
Foo f()
{
return Foo();
}
// NRVO example
Foo f()
{
Foo named_object;
return named_object;
}
Но при этом замысел и суть остаются такими же! Тут важно отметить, что и вам,
и компилятору, по объективным причинам, намного проще доказать
корректность RVO, чем NRVO.
// NRVO failed!
Foo f(bool value)
{
Foo a, b;
if (value)
return a;
else
return b;
}
// RVO works!
Foo f(bool value)
{
if (value)
return Foo();
else
return Foo();
}
Другой пример:
// RVO failed!
Foo f()
{
return Bar();
}
if (value)
return a; // move `a`
else
return b; // move `b`
}
Выводы:
-fno-elide-constructors