Привет, Хабр! Представляю вашему вниманию перевод статьи 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

Согласен, что этот код немного надуманный, и вряд ли Вы будете писать нечто подобное, однако сообщение об ошибке упоминает lvalue — термин, который не так часто увидишь в туториалах по C/C++. Другой пример нагляден при компиляции следующего кода при помощи 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'

Опять же, в сообщение об ошибке упоминается мистическое rvalue. Что же в C и C++ понимается под lvalue и rvalue? Это и есть тема данной статьи.

Простое определение


Для начала нарочито дадим определения lvalue и rvalue в упрощённой форме. В дальнейшем эти понятия будут рассмотрены под увеличительным стеклом.

lvalue (locator value) представляет собой объект, который занимает идентифицируемое место в памяти (например, имеет адрес).

rvalue определено путём исключения, говоря, что любое выражение является либо lvalue, либо rvalue. Таким образом из определения lvalue следует, что rvalue — это выражение, которое не представляет собой объект, который занимает идентифицируемое место в памяти.

Элементарные примеры


Термины, определённые выше, могут показаться немного нечёткими. Поэтому стоит сразу рассмотреть несколько простых поясняющих примеров. Предположим, мы имеем дело с переменной целого типа:

int var;
var = 4;

Оператор присваивания ожидает lvalue с левой стороны, и var является lvalue, потому что это объект с идентифицируемым местом в памяти. С другой стороны, следующие заклинания приведут к ошибкам:

4 = var;       // ERROR!
(var + 1) = 4; // ERROR!

Ни константа 4, ни выражение var + 1 не являются lvalue
(что автоматически их делает rvalue). Они не lvalue, потому что оба являются временным результатом выражений, которые не имеют определённого места в памяти (то есть они могут находится в каких-нибудь временных регистрах на время вычислений). Таким образом, присваивание в данном случае не несёт в себе никакого семантического смысла. Иными словами — некуда присваивать.

Теперь должно быть понятно, что означает сообщение об ошибке в первом фрагменте кода. foo возвращает временное значение, которое является rvalue. Попытка присваивания является ошибкой. То есть, видя код foo() = 2;, компилятор сообщает, что ожидает lvalue с левой стороны оператора присваивания.

Однако, не все присваивания результату вызова функции ошибочны. Например, использование ссылок в C++ делает это возможным:

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[] возвращает ссылку, которой может быть присвоено значение.

Изменяемые lvalue


Изначально, когда понятие lvalue было введено в C, оно буквально означало «выражение, применимое с левой стороны оператора присваивания». Однако позже, когда ISO C добавило ключевое слово const, это определение нужно было доработать. Действительно:

const int a = 10; // 'a' - lvalue
a = 10;           // но ему не может быть присвоено значение!  

Таким образом не всем lvalue можно присвоить значение. Те, которым можно, называются изменяемые lvalue (modifiable lvalues). Формально C99 стандарт определяет изменяемые lvalue как:
[...] lvalue, тип которого не является массивом, не является неполным, не имеет спецификатор const, не является структурой или объединением, содержащими поля (также включая поля, рекурсивно вложенные в содержащиеся агрегаты и объединения) со спецификатором const.

Преобразования между lvalue и rvalue


Образно говоря, конструкции языка, оперирующие значениями объектов, требуют rvalue в качестве аргументов. Например, бинарный оператор '+' принимает два rvalue в качестве аргументов и возвращает также rvalue:

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.

Что насчёт преобразования в другую сторону? Можно ли преобразовать rvalue в lvalue? Конечно нет! Это бы нарушило суть lvalue, согласно его определению (Отсутствие неявного преобразования означает, что rvalue не могут быть использованы там, где ожидается lvalue).

Это не означает, что lvalue не могут быть получены из rvalue явным способом. Например, унарный оператор '*' (разыменование) принимает rvalue в качестве аргумента, но возвращает lvalue в качестве результата. Рассмотрим следующий верный код:

int arr[] = {1, 2};
int* p = &arr[0];
*(p + 1) = 10;   // OK: p + 1 rvalue, однако *(p + 1) уже lvalue

Обратно, унарный оператор '&' (адрес) принимает lvalue как аргумент и производит rvalue:

int var = 10;
int* bad_addr = &(var + 1);  // ОШИБКА: требуется lvalue для унарного оператора '&'
int* addr = &var;            // ОК: var - lvalue
&var = 40;                   // ОШИБКА: требуется lvalue с левой стороны
                             //         оператора присваивания

Символ "&" играет несколько другую роль в C++ — он позволяет определить ссылочный тип. Его называют «ссылкой на lvalue». Неконстантной ссылке на lvalue не может быть присвоено rvalue, так как это потребовало бы неверное rvalue-в-lvalue преобразование:

std::string& sref = std::string();  // ОШИБКА: неверная инициализация             
                                    // неконстантной ссылки типа 'std::string&'
                                    // rvalue типа 'std::string'

Константным ссылкам на lvalue можно присвоить rvalue. Так как они константы, значение не может быть изменено по ссылке и поэтому проблема модификации rvalue просто отсутствует. Это свойство делает возможным одну из основополагающих идиом C++ — допуск значений по константной ссылке в качестве аргументов функций, что позволяет избежать необязательного копирования и создания временных объектов.

CV-специфицированные rvalues


Если прочесть внимательно часть С++ стандарта, касающуюся преобразования lvalue-в-rvalue (глава 4.1 в драфте стандарта C++11), то можно увидеть следующее:
lvalue (3.10) на тип T, не являющимся функциональным, или массивом, может быть преобразован в rvalue. [...] Если T не класс, типом rvalue является cv-неспецифицированная версия типа T. Иначе, типом rvalue является T.
Так что же значит «cv-неспецифицированный»? CV-спецификатор — это термин, используемый для описания const и volatile спецификаторов типа.

Из главы 3.9.3:
Каждый тип, который является 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 в действии.

Ссылки на rvalue (C++11)


Ссылки на rvalue и сопутствующий концепт семантики переноса являются одним из наиболее мощным инструментом, добавленным в язык C++11. Подробная дискуссия на эту тему выходит за рамки этой скромной статьи (вы можете найти кучу материала, просто погуглив «rvalue references». Вот некоторые ресурсы, которые я нахожу полезными: этот, этот и особенно вот этот), но всё же я хотел бы привести простой пример, потому что считаю, что данная глава является наиболее подходящим место, чтобы продемонстрировать как понимание lvalue и rvalue расширяет наши возможности рассуждать о нетривиальных концепциях языка.

Добрая половина статьи была потрачена на объяснение того, что одним из самых главных различий между lvalue и rvalue является тот факт, что lvalue можно изменять, в то время как rvalue — нет. Что же, C++11 добавляет одну важнейшую характерную особенность в этом различии, разрешая нам иметь ссылки на 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";

Хотя здесь я только присваиваю значение свеже созданному вектору, это является одной из демонстраций общего случая, когда некоторое временное rvalue создаётся и присваивается v2 (это может случится например, если функция возвращает вектор). Вот что мы увидим на экране:

assigning rvalue...
[0x28ff08] constructor
[0x28fef8] copy assignment operator
[0x28fec8] copy constructor
[0x28fec8] destructor
[0x28ff08] destructor
ended assigning rvalue...

Ого! Выглядит очень хлопотно. В частности, здесь потребовалась дополнительная пара вызовов конструктора с деструктором, чтобы создать, а потом удалить временный объект. И это печально, так как внутри копирующего оператора присваивания другой временный объект создаётся и удаляется. Дополнительная работа за зря.

Но, нет! C++11 даёт нам ссылки на 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;
}

Двойной асперсанд — это ссылка на rvalue. Он означает как раз то, что и обещает — даёт ссылку на rvalue, который будет уничтожен после вызова. Мы можем использовать этот факт, чтобы просто «стащить» внутренности rvalue — они ему всё равно не нужны! Вот, что выведется на экран:

assigning rvalue...
[0x28ff08] constructor
[0x28fef8] move assignment operator
[0x28ff08] destructor
ended assigning rvalue...

Как мы видим, вызывается новый переносящий оператор присваивания, так как rvalue присваивается v2. Вызовы конструктора и деструктора всё же необходимы для временного объекта, который создаётся через Intvec(33). Однако другой временный объект внутри оператора присваивания больше не нужен. Оператор просто меняет внутренний буфер rvalue со своим, и таким образом деструктор rvalue удаляет буфер самого объекта, который больше не будет использоваться. Чисто!

Хочу только отметить ещё раз, что этот пример только вершина айсберга семантики переноса и ссылок на rvalue. Как вы можете догадаться, это сложная тема с множеством частных случаев и загадок. Я пытался лишь продемонстрировать очень интересное применение различий между lvalue и rvalue в C++. Компилятор очевидно может их различать и позаботится о вызове правильного конструктора во время компиляции.

Заключение


Можно написать много C++ кода, не задумываясь о разногласиях rvalue и lvalue, опуская их как непонятный жаргон компилятора в сообщениях об ошибках. Однако, как я пытался показать в этой статье, лучшее владение этой темы обеспечит более глубокое понимание определённых конструкций C++, и сделает части стандарта C++ и дискуссии между экспертами языка для вас более доступными.

В стандарте C++11 эта тема является ещё более важной, так как C++11 вводит понятия ссылок на rvalue и семантики переноса. Чтобы действительно понять новые особенности языка, строгое понимание rvalue и lvalue просто необходимо.

Комментарии (30)


  1. Alex_Crack
    03.02.2018 09:58
    +4

    Спасибо за статью. Тема интересная, даже немного философская, т.к. не все задумываются во время написания кода, что казалось бы простые вещи могут быть довольно сложны внутри. Но знание позволяет избежать многих ошибок. А еще радует, что в последнее время на хабре стало появляться много годных статей по C++. Я, хоть и работаю веб-разработчиком, но от статей по js уже тошнит, особенно от очередных туториалов для новичков. А на C++ пишу для себя — он как отдушина, красивый мощный язык.


    1. rhaport Автор
      03.02.2018 10:24
      +1

      Спасибо. Сам программную в основном на Си. Для тестирования на различных уровнях стал применять C++ для написания тестовых фреймворков и собственно тестов. Ужасно удобно и приятно, однако обнаружил много пробелов в своих знаниях. Отсюда и мотивирующая часть.


    1. Fedcomp
      03.02.2018 11:31

      Попробуйте еще Rust.


    1. rkfg
      03.02.2018 21:36

      работаю веб-разработчиком
      А на C++ пишу для себя — он как отдушина, красивый мощный язык.

      У меня похожая ситуация, но можно попробовать сочетать одно с другим. Очень интересный фреймворк, похожий на GWT — весь код, включая клиентский, пишется на C++. Хотя он компонентно-ориентированный, т.е. больше подойдёт для разработки веб-приложений, а не сайтов.


      1. neit_kas
        04.02.2018 19:50

        Emscripten ещё можно предложить.


        1. rkfg
          04.02.2018 21:22

          Он вроде не для этого предназначен, а для кросс-компиляции уже существующих программ на C/C++, а Wt — это как Qt по духу, только для веба.


          1. neit_kas
            04.02.2018 21:27

            Это понятно. Я к вопросу: «сочетать одно с другим».


  1. rkfg
    03.02.2018 13:01
    +3

        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;
        }

    Тут оптимальнее переложить создание копии на компилятор, раз уж он всё равно позволяет это делать автоматически (получаем аргумент по значению, а не по ссылке):


        Intvec& operator=(Intvec other)
        {
            log("copy assignment operator");
            std::swap(m_size, other.m_size);
            std::swap(m_data, other.m_data);
            return *this;
        }

    На StackOverflow есть очень подробное и понятное объяснение copy&swap idiom.


  1. Mingun
    03.02.2018 13:05
    +1

    Вот некоторые ресурсы, которые я нахожу полезными: этот, этот и особенно вот этот)

    Перевод статьи по первой ссылке есть на Хабре.


    1. neit_kas
      04.02.2018 19:46

      Мне эта осознание дала.


  1. romy4
    03.02.2018 14:30

    Ох, если бы так же объяснили про std::move и std::forward


    1. Antervis
      03.02.2018 16:45
      +1

      std::move преобразует неконстантную lvalue-ссылку в rvalue-ссылку. Тип rvalue/const lvalue ссылок он не меняет. Пример: у std::vector есть две версии конструктора — принимающий lvalue и rvalue ссылки.

      vector<int> a = {1,2,3}; // a - lvalue-ссылка
      auto b = a; // Будет вызван vector(const vector &rhs); - копирование
      auto c = std::move(a); // Будет вызван vector(vector &&rhs); - перемещение
      

      std::forward тоже преобразует lvalue в rvalue ссылку, но только в том случае, если аргумент передан в функцию по rvalue ссылке.
      template <typename T>
      // может принять объект как по rvalue, так и по lvalue ссылке
      void bar(T &&v) {
          // всегда будет вызвано копирование, даже если v - rvalue
          // vector<int> a = v;
      
          // всегда будет вызвано перемещение, даже если v - lvalue
          // vector<int> a = move(v);
      
          // будет вызвано копирование для lvalue и перемещение для rvalue
          vector<int> a = forward<T>(v);
      }
      


      1. reversecode
        03.02.2018 17:11

        -


      1. rkfg
        03.02.2018 20:23
        +1

        Я бы ещё пояснил, что внутри функции T&& v эквивалентно T v (т.е. v передаётся по rvalue-ссылке, но принимается в обычное значение, а не в ссылку, типа T), так что чтобы прокинуть rvalue-ссылку дальше, нужен явный каст к T&&. std::forward имеет смысл только в шаблонах, насколько мне известно, т.к. в них применяется правило сжатия ссылок при выводе типа, так что такой шаблон можно использовать, как уже сказано, и для rvalue, и для lvalue. Если же мы явно укажем тип, то аргумент можно просто перемещать с помощью move, т.к. сжатие ссылок работать не будет.


        1. Antervis
          03.02.2018 22:38

          да там много можно объяснять. Я бы посоветовал почитать «Эффективный и современный с++» С. Майерса.


      1. romy4
        04.02.2018 02:13

        Что означает «перемещение» относительно вектора? На что будет указывать 'a' и что будет внутри 'a'?


        1. rkfg
          04.02.2018 12:03

          Перемещение — это передача по значению, только без копирования. Это будет тот же самый объект, что и до перемещения, вплоть до адреса в памяти, и содержимое будет точно то же самое.


          1. romy4
            04.02.2018 23:11

            Т.е., если использовать std::move, то изменив полученный объект, изменим исходный. Какой в этом смысл, если можно передать, например, указать или просто ссылку на объект?


            1. Deosis
              05.02.2018 10:13

              Использовав std::move, мы говорим, что аргумент нам больше не нужен, и обязуемся не использовать его.
              То есть использование объекта после передачи его в функцию — UB.


              1. romy4
                05.02.2018 21:48

                Спасибо. Значит move семантика — это просто часть логики обращения с объектом и его области видимости, времени жизни.


            1. rkfg
              05.02.2018 11:10

              Начнём с того, что move semantics так называется не ради красного словца. Помимо решения технической проблемы (исключение излишнего копирования), язык предоставляет нам новую семантику, смысл, который мы вкладываем в такую передачу аргумента. Когда значение передаётся по неконстантной ссылке, подразумевается, что функция будет изменять объект и, таким образом, оказывать влияние «вовне», т.е. её результат будет виден в месте, откуда функция вызывалась. Но это не всегда нужно, поэтому существует и передача по значению, когда функция получает собственный экземпляр объекта, и все операции над ним снаружи не окажутся (конечно, если функция как-либо явно не вернёт этот объект).


              До C++11 не было промежуточного варианта, когда мы и копировать не хотим, и «делиться» объектом тоже нежелательно. Что если нам «снаружи» объект уже не нужен, и мы хотим его просто передать внутрь «навсегда»? Конечно, можно передать по ссылке и, если объект был создан в пределах текущего скоупа, он автоматически будет уничтожен при выходе из него. Но передача по ссылке не несёт семантики поведения функции, при чтении такого кода неясно, можно ли дальше пользоваться переданным объектом или он был «испорчен» функцией (здесь «порча» понимается в терминах move semantics, т.е. содержимое объекта меняется, но его состояние остаётся допустимым для корректного уничтожения). Конечно, можно создать временную копию и передать ссылку на неё, но это пустая трата ресурсов. std::move() нужен для передачи объекта из рук в руки, он явно обозначает, что объект был перемещён, и далее в скоупе вызова им пользоваться нельзя, теперь им владеет вызванная функция, и она несёт за него ответственность.


              Хорошим примером будет unique_ptr. По своему смыслу, это уникальный, существующий в одном экземпляре указатель на объект. Тем не менее, им можно «поделиться» с помощью передачи по ссылке, но нельзя скопировать, копирующий конструктор удалён. А вот перемещающий конструктор есть, и он очень интересный: при перемещении указателя вызывается release() на аргументе конструктора, что приводит к обнулению указателя в этом аргументе. Пример:

              #include <iostream>
              #include <memory>
              
              using namespace std;
              
              class A {
              
              };
              
              void foo(unique_ptr<A> p) {
                  cout << "passed by value " << p.get() << endl;
              }
              
              int main(int argc, char* argv[]) {
                  auto a = make_unique<A>();
                  cout << "a was " << a.get() << endl;
                  foo(move(a));
                  cout << "a becomes " << a.get() << endl;
              }

              Выведет:


              a was 0x555555a61290
              passed by value 0x555555a61290
              a becomes 0

              Указатель переместился в функцию foo(), и снаружи неё его уже нельзя использовать! Использование std::move() явно говорит, что объект ушёл из этого скоупа куда-то дальше. Тут надо отметить, что если переписать сигнатуру foo как void foo(unique_ptr<A>&& p), то магия сломается, потому что перемещающий конструктор unique_ptr уже не вызовется, и для объекта такая передача будет полностью незаметна.


              В целом, как и const, move semantics обозначает намерения программиста и обещания функций. Можно свести это в таблицу:


              T — передача по значению, создастся копия, которая, возможно, будет меняться функцией; снаружи функции это не будет видно
              T& — передача по ссылке, объект, возможно, будет меняться функцией, и это будет видно снаружи
              const T& — передача по константной ссылке, объект не может меняться функцией (read-only доступ)
              T&& — передача по rvalue-ссылке, объект может меняться функцией, снаружи объект перестаёт существовать (или следует считать так, даже если он являлся lvalue и был перемещён с помощью std::move), копия не создаётся


              Несколько хороших примеров о приёме по значению и по rvalue-ссылке, а также применение std::move() есть в этом вопросе на StackOverflow, не всё так однозначно и чаще упирается в логику и намерения разработчика, чем в какие-то технические вопросы.


              1. romy4
                05.02.2018 21:47

                Крутейшее объяснение. Спасибо большое


        1. Antervis
          04.02.2018 21:23

          по стандарту, перемещение отличается от копирования тем, что исходный объект остается не в первоначальном состоянии, а в «любом корректном», это зависит от реализации перемещения в самом классе. Самый простой вариант — просто поменять местами поля данных двух объектов.


    1. dendron
      04.02.2018 19:37

      Если коротко: в шаблонных классах используйте std::forward, в остальных случаях — std::move.


  1. ivan386
    03.02.2018 16:29

    Я когда вникал что такое rvalue и lvalue наткнулся на такую рабочую кострукцию. В справочнике MSDN.


    ((a > b) ? a : b) = 1;


    1. LynXzp
      04.02.2018 00:25

      А что, так можно было? :) Спасибо.
      Странно, но gcc для C говорит: lvalue required as left operand of assignment (в С++ все хорошо).


      1. ivan386
        04.02.2018 01:21

        Сам был удивлён. Проверил на ближайшем онлайн компиляторе C++ и сработало.


      1. MikailBag
        05.02.2018 13:12

        Вот так работает.


        *((a > b) ? &a : &b) = 1;

        Ну т.е. берем указатель на переменную с большим значением и разыменовываем, получая lvalue.


        1. ivan386
          05.02.2018 17:00

          Так то логично что будет работать.


          1. MikailBag
            05.02.2018 20:24

            То что исходная конструкция работает в С++, вроде тоже логично.
            тип a это T&, тип b это T&.
            поэтому тернарный оператор возвращает T&, которому нормально присваевается значение.