Однократные rvalue, ссылки rvalue (&&) и семантика перемещения

переводчик C++ язык ассемблера


англоязычная версия опубликована в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Не стесняйтесь нажимать на другие мои статьи. Спасибо за чтение.