англоязычная версия опубликована вhackernoonи в моемблогАрхив.
Эта статья является китайским ремейком.
Во-первых, давайте посмотрим, что такоеC++ идет все дальше и дальше в производительности и расширяемости за счет простоты использования, что затрудняет изучение одной версии за другой. В этой статье в основном обсуждаются некоторые связанные знания о новой версии C++, rvalue, ссылка на rvalue (&&) и семантика перемещения, в надежде помочь вам решить эти трудности за один раз.
Правильное значение (r-значение)
Проще говоря, rvalue — это значение справа от знака равенства.
Код на:
int var; // too much JavaScript recently:)
var = 8; // OK! l-value (yes, there is a l-value) on the left
8 = var; // ERROR! r-value on the left
(var + 1) = 8; // ERROR! r-value on the left
Достаточно просто. Давайте рассмотрим более неясный случай, когда функции возвращают rvalue.
Код на:
#include <string>
#include <stdio.h>
int g_var = 8;
int& returnALvalue() {
return g_var; //here we return a left value
}
int returnARvalue() {
return g_var; //here we return a r-value
}
int main() {
printf("%d", returnALvalue()++); // g_var += 1;
printf("%d", returnARvalue());
}
результат:
8
9
Обратите внимание, что в примере функция возвращает lvalue только для демонстрационных целей, пожалуйста, не имитируйте ее в реальной жизни.
Что такое использование rvalue
На самом деле правильное значение уже влияет на логику кода еще до появления ссылки на правильное значение (&&).
Например, эта строка кода:
const std::string& name = "rvalue";
Нет проблем, но эта строка:
std::string& name = "rvalue"; // use a left reference for a rvalue
не компилируется:
error: non-const lvalue reference to type 'std::string' (aka 'basic_string<char, char_traits<char>, allocator<char> >') cannot bind to a value of unrelated type 'const char [7]'
Объясняет, что компилятор заставляет нас использовать ссылку const для указания на значение r.
Еще одно интересное 🌰:
#include <stdio.h>
#include <string>
void print(const std::string& name) {
printf("rvalue detected:%s\n", name.c_str());
}
void print(std::string& name) {
printf("lvalue detected:%s\n", name.c_str());
}
int main() {
std::string name = "lvalue";
print(name); //compiler can detect the right function for lvalue
print("rvalue"); // likewise for rvalue
}
результат операции:
lvalue detected:lvalue
rvalue detected:rvalue
Объяснения этой разницы достаточно, чтобы компилятор решил перегрузить функцию.
Значит, rvalue — константы?
не полностью. Теперь твоя очередь&&
появившийся.
Код на:
#include <stdio.h>
#include <string>
void print(const std::string& name) {
printf(“const value detected:%s\n”, name.c_str());
}
void print(std::string& name) {
printf(“lvalue detected%s\n”, name.c_str());
}
void print(std::string&& name) {
printf(“rvalue detected:%s\n”, name.c_str());
}
int main() {
std::string name = “lvalue”;
const std::string cname = “cvalue”;
print(name);
print(cname);
print(“rvalue”);
}
результат операции:
lvalue detected:lvalue
const value detected:cvalue
rvalue detected:rvalue
Это означает, что если есть функция, перегруженная для rvalue, параметр rvalue выберет выделенную функцию (accept&&
параметр), а не более общая функция, которая принимает постоянные ссылки в качестве параметров. так,&&
ссылки rvalue и const можно уточнить.
Я подытожил адаптивность фактического параметра функции (фактически переданная переменная) и формального параметра (переменная, объявленная в скобках), если вам интересно, вы также можете проверить это, изменив вышеприведенное 🌰:
Разбивать ссылки на константы на ссылки на константы и значения r — это здорово, но это все еще не отвечает на вопрос.
Какую проблему решает &&?
Проблема заключается в ненужной глубокой копии, когда параметр является значением r.
Быть конкретными,&&
Используется для различения rvalue, так что, когда rvalue 1) является параметром конструктора или функции присваивания и 2) соответствующий класс содержит указатель и указывает на динамически выделяемый ресурс (память), его можно избежать внутри функции deep копировать.
Если вы используете код, вы можете быть более конкретным:
#include <stdio.h>
#include <string>
#include <algorithm>
using namespace std;
class ResourceOwner {
public:
ResourceOwner(const char res[]) {
theResource = new string(res);
}
ResourceOwner(const ResourceOwner& other) {
printf("copy %s\n", other.theResource->c_str());
theResource = new string(other.theResource->c_str());
}
ResourceOwner& operator=(const ResourceOwner& other) {
ResourceOwner tmp(other);
swap(theResource, tmp.theResource);
printf("assign %s\n", other.theResource->c_str());
}
~ResourceOwner() {
if (theResource) {
printf("destructor %s\n", theResource->c_str());
delete theResource;
}
}
private:
string* theResource;
};
void testCopy() {
// case 1
printf("=====start testCopy()=====\n");
ResourceOwner res1("res1");
ResourceOwner res2 = res1;
//copy res1
printf("=====destructors for stack vars, ignore=====\n");
}
void testAssign() {
// case 2
printf("=====start testAssign()=====\n");
ResourceOwner res1("res1");
ResourceOwner res2("res2");
res2 = res1;
//copy res1, assign res1, destrctor res2
printf("=====destructors for stack vars, ignore=====\n");
}
void testRValue() {
// case 3
printf("=====start testRValue()=====\n");
ResourceOwner res2("res2");
res2 = ResourceOwner("res1");
//copy res1, assign res1, destructor res2, destructor res1
printf("=====destructors for stack vars, ignore=====\n");
}
int main() {
testCopy();
testAssign();
testRValue();
}
результат операции:
=====start testCopy()=====copy res1=====destructors for stack vars, ignore=====destructor res1destructor res1=====start testAssign()=====copy res1assign res1destructor res2=====destructors for stack vars, ignore=====destructor res1destructor res1=====start testRValue()=====copy res1assign res1destructor res2destructor res1=====destructors for stack vars, ignore=====destructor res1
первые два примераtestCopy()
иtestAssign()
Результаты внутри прекрасны. здесь будетres1
Ресурсы внутри копируются вres2
Это разумно, потому что обе эти отдельные сущности должны иметь свой собственный эксклюзивный ресурс (строку).
Но в третьем примере это не так. Объект res1 этой глубокой копии является значением rvalue (ResourceOwner(“res1”)
возвращаемое значение), на самом деле оно скоро будет переработано. Так что нет необходимости в эксклюзивных ресурсах.
Повторю еще раз описание проблемы, на этот раз оно должно быть понятным:
&&
Используется для различения rvalue, так что, когда rvalue 1) является параметром конструктора или функции присваивания, и 2) соответствующий класс содержит указатель и указывает на динамически выделяемый ресурс (память), его можно избежать внутри функции deep копировать.
Если глубокое копирование ресурсов rvalue нецелесообразно, какие операции разумны? ответ
Move
Продолжайте обсуждать семантику движений. Решение очень простое, если параметр rvalue, то он не копирует, а напрямую "перемещает" ресурс. Давайте сначала перегрузим функцию присваивания ссылкой на rvalue:
ResourceOwner& operator=(ResourceOwner&& other) {
theResource = other.theResource;
other.theResource = NULL;
}
Эта новая функция присваивания называетсяфункция присваивания перемещения.конструктор перемещенияЭто также может быть достигнуто аналогичным способом, который не будет повторяться здесь.
Если это не просто понять, можно сделать так: Например, если вы продаете старый дом и переезжаете в новый, вам не нужно выбрасывать всю мебель и покупать новую при переезде (мы потерял его в 🌰3). Вы также можете «перевезти» мебель в новый дом.
Идеально.
Тогда что такое std::move?
Наконец, мы обращаемся к этому std::move.
Сначала разберемся с проблемой:
Когда 1) мы знаем, что аргумент является значением r, но 2) компилятор этого не знает, аргумент не может быть вызван для перегруженной функции перемещения.
Распространенной ситуацией является добавление слоя класса к владельцу ресурса.ResourceHolder
holder
|
|----->owner
|
|----->resource
Обратите внимание, что в приведенном ниже коде я поставилконструктор перемещенияТакже добавлено.
Код на:
#include <string>
#include <algorithm>
using namespace std;
class ResourceOwner {
public:
ResourceOwner(const char res[]) {
theResource = new string(res);
}
ResourceOwner(const ResourceOwner& other) {
printf(“copy %s\n”, other.theResource->c_str());
theResource = new string(other.theResource->c_str());
}
++ResourceOwner(ResourceOwner&& other) {
++ printf(“move cons %s\n”, other.theResource->c_str());
++ theResource = other.theResource;
++ other.theResource = NULL;
++}
ResourceOwner& operator=(const ResourceOwner& other) {
ResourceOwner tmp(other);
swap(theResource, tmp.theResource);
printf(“assign %s\n”, other.theResource->c_str());
}
++ResourceOwner& operator=(ResourceOwner&& other) {
++ printf(“move assign %s\n”, other.theResource->c_str());
++ theResource = other.theResource;
++ other.theResource = NULL;
++}
~ResourceOwner() {
if (theResource) {
printf(“destructor %s\n”, theResource->c_str());
delete theResource;
}
}
private:
string* theResource;
};
class ResourceHolder {
……
ResourceHolder& operator=(ResourceHolder&& other) {
printf(“move assign %s\n”, other.theResource->c_str());
resOwner = other.resOwner;
}
……
private:
ResourceOwner resOwner;
}
существуетResourceHolder
изфункция присваивания перемещения, на самом деле то, что мы хотим назватьфункция присваивания перемещения, поскольку члены rvalue также являются rvalue. но
resOwner = other.resOwner
Фактически вызывается обычная функция присваивания, либо делается глубокая копия.
Затем повторите вопрос еще раз, чтобы убедиться, что его легко понять:
Когда 1) мы знаем, что аргумент является значением r, но 2) компилятор этого не знает, аргумент не может быть вызван для перегруженной функции перемещения.
В качестве обходного пути мы можем использоватьstd::move
Приведение этой переменной к rvalue вызовет правильную перегруженную функцию.
ResourceHolder& operator=(ResourceHolder&& other) {
printf(“move assign %s\n”, other.theResource->c_str());
resOwner = std::move(other.resOwner);
}
Можем ли мы копнуть немного глубже?
абсолютно нормально!
Все мы знаем, что помимо отключения компилятора, принудительное преобразование фактически сгенерирует соответствующий машинный код. (Легче наблюдать, не открывая O) Эти машинные коды перемещают переменные в регистрах разного размера для фактического выполнения операции приведения.
такstd::move
Вы сделали то же самое с принудительным переводом? Не знаю, давай попробуем вместе.
Во-первых, давайте изменим основную функцию (я стараюсь сохранить логику)
Код на:
int main() {
ResourceOwner res(“res1”);
asm(“nop”); // remeber me
ResourceOwner && rvalue = std::move(res);
asm(“nop”); // remeber me
}
Скомпилируйте его, затем введите язык ассемблера с помощью следующей команды
clang++ -g -c -std=c++11 -stdlib=libc++ -Weverything move.cc
gobjdump -d -D move.o
😯, оригинальный стиль рисования, скрытый ниже, выглядит так:
0000000000000000 <_main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 20 sub $0x20,%rsp
8: 48 8d 7d f0 lea -0x10(%rbp),%rdi
c: 48 8d 35 41 03 00 00 lea 0x341(%rip),%rsi # 354
<GCC_except_table5+0x18>
13: e8 00 00 00 00 callq
18 <_main+0x18> 18: 90 nop // remember me
19: 48 8d 75 f0 lea -0x10(%rbp),%rsi
1d: 48 89 75 f8 mov %rsi,-0x8(%rbp)
21: 48 8b 75 f8 mov -0x8(%rbp),%rsi
25: 48 89 75 e8 mov %rsi,-0x18(%rbp)
29: 90 nop // remember me
2a: 48 8d 7d f0 lea -0x10(%rbp),%rdi
2e: e8 00 00 00 00 callq 33 <_main+0x33>
33: 31 c0 xor %eax,%eax
35: 48 83 c4 20 add $0x20,%rsp
39: 5d pop %rbp
3a: c3 retq
3b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
Я тоже не понимаю, но работаетnop
окрашенный. увидеть дваnop
Средняя часть действительно генерирует некоторый машинный код, но эти машинные коды, похоже, ничего не делают, а просто присваивают адрес одной переменной другой. И, если мы включим O (достаточно -O1), весь машинный код в середине nop будет убит.
clang++ -g -c -O1 -std=c++11 -stdlib=libc++ -Weverything move.cc
gobjdump -d -D move.o
Опять же, если вы измените ключевую строку на
ResourceOwner & rvalue = res;
За исключением того, что относительное смещение переменных изменилось, сгенерированный машинный код фактически остался прежним.
объяснил здесьstd::move
На самом деле это чистый синтаксический сахар, и здесь нет реальной операции.
Что ж, давайте сегодня напишем сюда. Если вам понравилась эта статья, ставьте лайк и подписывайтесь. Вы также можете пойти, если вы заинтересованыMediumНе стесняйтесь нажимать на другие мои статьи. Спасибо за чтение.