Что такое семантика перемещения?

В вашем коде это невозможно, если B продолжит A

public class TestSuper {
    public static class A {
        @Override
        public String toString() { return "I am A"; }
    }
    public static class B {
        @Override
        public String toString() { return "I am B"; }
    }
    public static void main(String[] args) {
        B b = new B();
        System.out.println( b ); // --> I am B
        A a = (A)b;
        System.out.println( a ); 
    }
} 
1569
задан L. F. 12 May 2019 в 09:30
поделиться

6 ответов

Я считаю, что легче всего понять семантику перемещения на примере кода. Начнем с очень простого строкового класса, который содержит только указатель на выделенный в куче блок памяти:

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = std::strlen(p) + 1;
        data = new char[size];
        std::memcpy(data, p, size);
    }

Поскольку мы сами решили управлять памятью, нам нужно следовать правилу трех . Я собираюсь отложить написание оператора присваивания и пока реализую только деструктор и конструктор копирования:

    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = std::strlen(that.data) + 1;
        data = new char[size];
        std::memcpy(data, that.data, size);
    }

Конструктор копирования определяет, что означает копирование строковых объектов. Параметр const string & that привязывается ко всем выражениям типа string, что позволяет вам делать копии в следующих примерах:

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

Теперь ключевая информация о семантике перемещения. Обратите внимание, что только в первой строке, где мы копируем x , эта глубокая копия действительно необходима, потому что мы, возможно, захотим проверить x позже и будем очень удивлены, если x ] как-то изменилось. Вы заметили, как я сказал x три раза (четыре раза, если вы включите это предложение) и каждый раз имел в виду один и тот же объект ? Мы называем такие выражения, как x «lvalues».

Аргументы в строках 2 и 3 являются не l-значениями, а r-значениями, потому что базовые строковые объекты не имеют имен, поэтому у клиента нет возможности проверить их снова в более поздний момент времени. rvalues ​​обозначают временные объекты, которые уничтожаются следующей точкой с запятой (точнее: в конце полного выражения, которое лексически содержит rvalue).Это важно, потому что во время инициализации b и c мы могли делать с исходной строкой все, что хотели, и клиент не мог заметить разницы !

В C ++ 0x представлен новый механизм, называемый «ссылка rvalue», который, среди прочего, позволяет нам обнаруживать аргументы rvalue через перегрузку функции. Все, что нам нужно сделать, это написать конструктор со ссылочным параметром rvalue. Внутри этого конструктора мы можем делать все, что захотим с источником, пока мы оставляем его в некотором допустимом состоянии:

    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

Что мы здесь сделали? Вместо того, чтобы глубоко копировать данные кучи, мы просто скопировали указатель, а затем установили исходный указатель в значение null (чтобы предотвратить удаление "только что украденных данных" с помощью 'delete []' из деструктора исходного объекта). Фактически, мы «украли» данные, которые изначально принадлежали исходной строке. Опять же, ключевой момент заключается в том, что клиент ни при каких обстоятельствах не может обнаружить, что источник был изменен. Поскольку на самом деле мы здесь не копируем, мы называем этот конструктор «конструктором перемещения». Его задача - перемещать ресурсы от одного объекта к другому, а не копировать их.

Поздравляем, теперь вы понимаете основы семантики перемещения! Продолжим реализацию оператора присваивания. Если вы не знакомы с идиомой копирования и обмена , изучите ее и возвращайтесь, потому что это прекрасная идиома C ++, связанная с безопасностью исключений.

    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

А, вот и все? "Где ссылка на rvalue?" вы можете спросить."Нам это здесь не нужно!" это мой ответ :)

Обратите внимание, что мы передаем параметр , который , по значению , поэтому , что должен быть инициализирован так же, как любой другой строковый объект. Как именно и будут инициализированы? В былые времена C ++ 98 ответ был бы «конструктором копирования». В C ++ 0x компилятор выбирает между конструктором копирования и конструктором перемещения в зависимости от того, является ли аргумент оператора присваивания lvalue или rvalue.

Итак, если вы скажете a = b , конструктор копирования инициализирует , что (поскольку выражение b является lvalue) , а оператор присваивания заменяет содержимое только что созданной глубокой копией. Это само определение идиомы копирования и обмена - сделайте копию, замените содержимое копией, а затем избавьтесь от копии, покинув область видимости. Здесь ничего нового.

Но если вы скажете a = x + y , конструктор перемещения инициализирует , что (потому что выражение x + y является rvalue), поэтому глубокая копия не требуется, только эффективный ход. , что все еще является независимым объектом от аргумента, но его конструкция была тривиальной, поскольку данные кучи не нужно было копировать, их просто перемещали. Копировать его не было необходимости, потому что x + y - это rvalue, и, опять же, можно перейти от строковых объектов, обозначенных rvalues.

Подводя итог, конструктор копирования делает глубокую копию, потому что источник должен оставаться нетронутым.Конструктор перемещения, с другой стороны, может просто скопировать указатель, а затем установить указатель в источнике на null. Это нормально, чтобы «обнулить» исходный объект таким образом, потому что у клиента нет возможности проверить объект снова.

Я надеюсь, что этот пример уловил суть.Существует гораздо больше возможностей rvalue ссылок и семантики перемещения, которые я намеренно упустил, чтобы не усложнять. Если вам нужны подробности, см. мой дополнительный ответ .

2363
ответ дан 22 November 2019 в 20:13
поделиться

Семантика перемещения основана на ссылках на rvalue .
Rvalue - это временный объект, который будет уничтожен в конце выражения. В текущем C ++ rvalue привязываются только к ссылкам const . C ++ 1x допускает не- const ссылки rvalue, записанные T && , которые являются ссылками на объекты rvalue.
Поскольку rvalue умирает в конце выражения, вы можете украсть его данные . Вместо копирования его в другой объект, вы перемещаете в него его данные.

class X {
public: 
  X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
    : data_()
  {
     // since 'x' is an rvalue object, we can steal its data
     this->swap(std::move(rhs));
     // this will leave rhs with the empty data
  }
  void swap(X&& rhs);
  // ... 
};

// ...

X f();

X x = f(); // f() returns result as rvalue, so this calls move-ctor

В приведенном выше коде со старыми компиляторами результат f () копируется в x с использованием копии X конструктор. Если ваш компилятор поддерживает семантику перемещения и X имеет конструктор перемещения, то он вызывается вместо этого. Поскольку его аргумент rhs является rvalue , мы знаем, что он больше не нужен, и можем украсть его значение.
Таким образом, значение перемещается из безымянного временного объекта, возвращенного из f () , в x (в то время как данные x инициализируются как пустой X , перемещается во временное, которое будет уничтожено после присвоения).

76
ответ дан 22 November 2019 в 20:13
поделиться

Предположим, у вас есть функция, которая возвращает существенный объект:

Matrix multiply(const Matrix &a, const Matrix &b);

Когда вы пишете такой код:

Matrix r = multiply(a, b);

, тогда обычный компилятор C ++ создает временный объект для результата multiply () вызовите конструктор копирования для инициализации r , а затем уничтожьте временное возвращаемое значение. Семантика перемещения в C ++ 0x позволяет вызывать «конструктор перемещения» для инициализации r путем копирования его содержимого, а затем отбрасывать временное значение без необходимости его уничтожения.

Это особенно важно, если (как, например, пример Matrix выше) копируемый объект выделяет дополнительную память в куче для хранения своего внутреннего представления. Конструктор копирования должен либо сделать полную копию внутреннего представления, либо использовать подсчет ссылок и семантику копирования при записи во взаимодействии. Конструктор перемещения оставит память кучи в покое и просто скопирует указатель внутри объекта Matrix .

59
ответ дан 22 November 2019 в 20:13
поделиться

Если вас действительно интересует товар, подробное объяснение семантики перемещения, я настоятельно рекомендую прочитать исходную статью о них, «Предложение о добавлении поддержки семантики перемещения в язык C ++».

Это очень доступно и легко читается, и это делает отличный пример преимуществ, которые они предлагают. Есть и другие, более свежие и актуальные статьи о семантике перемещения, доступные на веб-сайте WG21 , но эта, вероятно, самая прямолинейная, поскольку она подходит к вещам с точки зрения верхнего уровня и не дает многого. в суровые языковые детали.

30
ответ дан 22 November 2019 в 20:13
поделиться

Это похоже на семантику копирования, но вместо того, чтобы дублировать все данные, вы должны украсть данные из объекта, из которого "перемещается".

19
ответ дан 22 November 2019 в 20:13
поделиться

Вы ведь знаете, что означает семантика копирования? это означает, что у вас есть типы, которые можно копировать, для определяемых пользователем типов вы определяете это либо явно, написав конструктор копирования и оператор присваивания, либо компилятор генерирует их неявно. Это сделает копию.

Семантика перемещения - это в основном определяемый пользователем тип с конструктором, который принимает ссылку на r-значение (новый тип ссылки с использованием && (да, два амперсанда)), которая не является константой, это называется конструктором перемещения, то же самое касается оператор присваивания. Итак, что делает конструктор перемещения: вместо того, чтобы копировать память из исходного аргумента, он «перемещает» память из источника в место назначения.

Когда бы вы хотели это сделать? ну, пример std :: vector, допустим, вы создали временный std :: vector и возвращаете его из функции, скажем:

std::vector<foo> get_foos();

У вас будут накладные расходы от конструктора копирования, когда функция вернется, если (и он будет в C ++ 0x) std :: vector имеет конструктор перемещения вместо копирования, он может просто установить его указатели и «переместить» динамически выделенную память в новый экземпляр. Это что-то вроде семантики передачи права собственности с std :: auto_ptr.

13
ответ дан 22 November 2019 в 20:13
поделиться
Другие вопросы по тегам:

Похожие вопросы: