Pull to refresh

Comments 33

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

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

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

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

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

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

Это понятно. Я к вопросу: «сочетать одно с другим».
    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.

Ох, если бы так же объяснили про std::move и std::forward
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);
}
UFO just landed and posted this here

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

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

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

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

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

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

Начнём с того, что 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, не всё так однозначно и чаще упирается в логику и намерения разработчика, чем в какие-то технические вопросы.

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

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


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

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

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


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

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

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

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

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

Я всегда считал, что lvalue — это «left value», а rvalue — «right value».
lvalue — то, куда присваивается, rvalue — то, что присваивается, т.е. lvalue = rvalue.
По-моему очень удобное определение.
Однако это не совсем так. Смотрите, например, главу «Изменяемые lvalue».
Да, понятно, что на lvalue/rvalue накладываются какие-то условия.
В той же главе пишется:
Таким образом не всем lvalue можно присвоить значение


Например в случае:
const int val = 0;
val = 4; // Тут val все еще lvalue, хоть и нельзя так писать
Sign up to leave a comment.

Articles