Comments 38
Очень интересно! Спасибо!
Отличный перевод, отличной статьи, причем довольно свежей.
"Таким образом, этот «знак» (ссылочный тип rvalue) сообщает компилятору, что нужно выбрать перемещающий конструктор", подумал тут особенность перевода, но нет - тут неточность у автора (дайте немного подушнить): std::move возвращает xvalue, а не просто "rvalue ссылочный тип" и вот xvalue будет подкатегорией выражения и она может отличаться от rvalue (наряду с prvalue). Дальше это показывается на диаграмме, а у меня там белый блок вместо картинки, и получается, что в тексте формулировка размыта.
Отличная статья. Я бы только дополнил рекомендацией:
Стремитесь создавать классы, где не требуется переопределять специальные функции-члены.
Про правило пяти: стоит расписать в каких случаях автоматически создаются либо не создаются конструкторы и операторы присвоения.
А как современный C++ относится к последнему примеру, где мы в конструкторе Derived фактически сначала опустошили other, переместив его в конструктор базового класса, а затем читаем одно из его полей? Во всем этом длинном рассказе ничего не было про частичное перемещение типов
Было про "неспецифичное состояние".
Ну так получается, используя таким образом объект, у нас нет никаких гарантий, что произойдет все так, как задумано (в реальности конечно вряд ли, но C++ вообще полон вещей, которые в реальности вряд ли случатся, но тем не менее запрещены)?
Если в конструкторе Derived (или derived_data_) пытаться обращаться к элементам базового класса, то проблема с тем, что базовый объект перемещен, конечно будет. А если не обращаться, то вроде нормально.
Перемещается же весь объект, а не его часть. Поэтому получается, что и обращение к членам Derived класса перемещенного объекта не гарантированно что-то даст. Во всяком случае, с точки зрения Стандарта (интересно, нашелся ли уже энтузиаст, который принципиально делает компилятор C++, который во всех местах, позволенных стандартом, ломает код...). Вот интересно, как в Стандарте предлагают решать эту проблему.
Тут вообще-то вся статья рассказывает, что move ничего не перемещает. А конструктор по ссылке базового класса может изменить только что-то в базовом классе.
Сама функция не перемещает. Но навешивает флаг, что после передачи в конструктор базового класса компилятор помечает объект перемещенным. Флаг же на весь объект навешивается, а не на его часть
Откуда взялось "компилятор помечает объект перемещенным"? Объект помечается как rvalue, что позволяет передать его в функцию, принимающую rvalue ссылку. И ничего больше.
Ну так после передачи "в функцию, принимающую rvalue ссылку" здесь объекта больше нет. Он перемещен в эту функцию. Даже если она пустая (а в общем случае компилятор не знает, что внутри функции -- брала она данные из объекта или не брала), логически-то объект после этого пустой. И использовать его после этого -- ну, дурно пахнет.
Объект помечается как rvalue
Ну так я так и говорю:
Но навешивает флаг,
Передача объекта с флагом заканчивает его (время жизни? Не совсем, он же не разрушается. Скажем, "полезность") в точке передачи, после передачи компилятор помечает его перемещенным.
Объект никуда не перемещен. И компилятор ничего, кроме передачи объекта в функцию, с ним не делает. Как и любое другое приведение типа ничего само не делает с исходным объектом. Функция может делать с объектом что угодно, но оставить его в валидном состоянии. Например, после a = std::move(b); в b может оказаться значение, бывшее до присваивания в a - если поменять значения местами дешевле, чем создать пустое значение.
Но функция тут - конструктор базового класса. Она получает ссылку на базовый класс, и не может ничего сделать с полями производного класса.
Объект никуда не перемещен.
Физически. А логически очень даже перемещен, иначе зачем функция называется move? По-приколу?
Например, после a = std::move(b); в b может оказаться значение, бывшее до присваивания в a - если поменять значения местами дешевле, чем создать пустое значение.
И что вы будите дальше с этим делать? Вот представьте, что в a чувствительные данные, вы правда хотите организовать их утечку через b? Поэтому использовать b после этого -- плохой тон.
Она получает ссылку на базовый класс, и не может ничего сделать с полями производного класса.
Кто вам сказал? Нет никаких запретов на то, что может делать функция.
По-приколу?
Ну не совсем по приколу, но тут в тексте написано "Во всей стандартной библиотеке C++ эта функция, пожалуй, названа наиболее превратно."
Поэтому использовать b после этого -- плохой тон.
Осмысленно использовать b затруднительно исключительно потому, что мы не знаем, какое в нем после этого лежит значение.
Нет никаких запретов на то, что может делать функция.
Как функция базового класса может что-то сделать с полем, определенном в производном классе?
Осмысленно использовать b затруднительно
Просто записать в нее новое значение
Осмысленно использовать b затруднительно исключительно потому, что мы не знаем, какое в нем после этого лежит значение.
Вот именно. Но ведь в качестве идеоматического подхода реализации конструктора Derived(Derived&&) нам именно это и предлагают сделать. Сомневаюсь, что хоть в каком-то месте это будет удовлетворительное поведение
Как функция базового класса может что-то сделать с полем, определенном в производном классе?
static_cast / dynamic_cast / вызов виртуального метода у other?
После вызова перемещающего конструктора для базового класса мы не знаем, какие значения лежат в other в полях базового класса. А поля производного не изменились.
static_cast / dynamic_cast к производному классу в конструкторе? Это дичь какая-то. Конечно умышленно навредить можно. Но в реальном коде такого не может быть. Вызов виртуального метода у other? Ну тут может быть даже можно какой-нибудь искусственный пример придумать. Но в этом конкретном случае виртуальных методов нет. То есть в каких-то малореальных искусственных ситуациях при вызове
Derived(Derived&& other) : Base(std::move(other))
конструктор базового класса может изменить поля в Derived, но сам по себе вызов move ничего не портит. Это эквивалентно
Derived(Derived&& other) : Base((Base&&)other).
А поля производного не изменились.
Стандарт об этом ничего не говорит, они часть перемещенного объекта и следовательно, там случайное значение.
Но в реальном коде такого не может быть.
К сожалению, компилятор не может полагаться на статистику и рейтинги говнокодости. В реальности и исключения в при выполнении конструкторов кидаются редко (а из move конструкторах и того реже), но за exception safety почему-то топят.
но сам по себе вызов move ничего не портит
Так никто и не утверждает, что портит move. Испортить может Base(Base&&), а компилятор может не видеть, что в теле этого конструктора делается.
Кстати, с помощью CRTP базовый класс вполне может легально пользоваться полями унаследованного и никто ничего плохого в этом паттерне не видит.
А может попробовать написать компилируемый полный пример?
Мне кажется, не получится
Ну так после передачи "в функцию, принимающую rvalue ссылку" здесь объекта больше нет.
Не правда.
Он перемещен в эту функцию
Нет
логически-то объект после этого пустой
Не обязательно пустой. В неизвестном валидном состоянии.
И использовать его после этого -- ну, дурно пахнет.
Можно использовать, если нам не важно, что в нем.
Передача объекта с флагом заканчивает его ...
Ничего не заканчивает
после передачи компилятор помечает его перемещенным.
Никак не помечает
Не обязательно пустой. В неизвестном валидном состоянии.
То есть в случайном
Можно использовать, если нам не важно, что в нем.
Но в данном случае-то важно. Если не важно, зачем вообще в конструкторе Derived из него данные брать?
Ничего не заканчивает
Тогда бы состояние объекта не менялось, а оно меняется
Никак не помечает
То, что он вам об этом не сообщает, не значит, что этого не делает. Как вы отличите объект в неспецифицированном состоянии от объекта в специфицированном? А если различать не нужно, то зачем вообще вводить это состояние?
Да, в случайном.
И нет, не важно стало что в базовом классе - пока в конструкторе Derived не берутся данные из базового класса.
Не move меняет состояние объекта, а конструктор.
"в неспецифицированном состоянии" - это только о том, что стандарт не оговаривает, какое значение будет. И компилятору не нужно ничего различать.
До того, как в с++ завезли rvalue ссылки, был такой auto_ptr и у него обычный оператор присваивания менял значение аргумента: после a = b; значение b занулялось - он использовал обычную lvalue ссылку. И это разрыв шаблона для программиста. А компилятору всё равно. Так вот если вы в конструкторе поменяете &&other на &other и уберете std::move, то с точки зрения компилятора код надо генерировать точно такой же. А у программиста будет некоторое недоумение по поводу неожиданного изменения параметра.
пока в конструкторе Derived не берутся данные из базового класса.
Они и не берутся
И компилятору не нужно ничего различать.
Компилятор в принципе и ошибки не обязан вам объяснять, достаточно просто сказать, что что-то не так. А в 2026 ожидаешь от компилятора помощи, а не насмешек по поводу недостаточно полного знания талмуда.
Не move меняет состояние объекта, а конструктор.
Об этом никто и не спорит
Derived(Derived&& other)
// Вызов Base(&&) меняет состояние other
// теперь там неизвестно что
: Base(std::move(other))
// берем из неизвестно чего
, derived_data_(std::move(other.derived_data_))
Обязательный пропуск копий в C++17
Необычный термин... Я привык к избеганию копирования или чему-то подобному.
Кстати, странно, что в статье ни разу не прозвучало шикарное название std::move от Скотта Майерса - rvalue_cast. По-моему, это всё объясняет сразу. А вот подробности, на что там делится rvalue в современной классификации, для понимания именно мув-семантики не нужны - это действительно нужно для понимания copy elision, temporary materialization, etc, но в мув-семантику это подмешивать не стоит.
Ну и странно, что зачем-то автор помянул форвард. Без универсальных ссылок, без правил схлопывания ссылок, без объяснения явных шаблонных аргументов или, собственно, реализации... Только лишняя сущность в статье получилась.
Да уж... Столько хорошей информации и правильных слов в самой статье, а потом хоба! И реализация DynamicArray в которой конструктор копирования не exception safe :(((
PS. Поскольку DynamicArray -- это всего лишь демонстрация, то придираться к тому, что он поддерживает только те T, которые являются DefaultConstructible, нет смысла.
Как конструктор копирования может быть exception safe, если надо выделять память?
На мой рабоче-крестьянский взгляд exception safety означает не отсутствие исключений, а отсутствие проблем при возникновении исключений. В данной статье показан конструктор копирования DynamicArray, который не обеспечивает никаких гарантий exception safety:
DynamicArray(const DynamicArray& other)
: data_(new T[other.capacity_]),
size_(other.size_),
capacity_(other.capacity_) {
// Кто будет освобождать память по указателю data_ если в одной
// из строчек ниже возникнет исключение?
std::cout << "Copy constructor (copying " << size_ << " elements)\n";
std::copy(other.data_, other.data_ + size_, data_);
}
Мда, читаешь это и понимаешь, что C++ - неизучаемый язык. Просто какой-то кошмар.
"У нас статическая типизация!" - кричали они. Есть система типов, это основа, ведь это так просто! А потом оказывается, что помимо типов есть квалификаторы типов. Зачем эта штука нужна? Сообщать компилятору о намерениях программиста и позволять проводить оптимизации. Хорошо, мы начинаем учитывать квалификаторы типов. Но нам говорят: подождите, это еще не все! У нас есть типы, есть квалификаторы типов, давайте еще введем категории в системе типов! Что это такое мы вам толком объяснить не можем, но поверьте нам, учитывать эту хрень вам придется! И да, категории тоже сообщают компилятору о намерениях программиста и влияют на оптимизации. Ну вот так получилось.
Тут выходит на сцену Dan Saks и пишет статьи "Lvalues and Rvalues" и "Non-modifiable Lvalues", и как бы говорит: спокуха, ребята, категории - это просто! Вам всего-то надо разобраться с lvalues и rvalues. Да, да, мы понимаем, что это внутренние сущности компилятора, но мы решили, что прикладные программисты должны тоже ломать себе голову: компиляторщики не хотят сидеть в одиночестве! А на вопрос: да что же это такое, автор отвечает: "Большинство книг по C или C++ объясняют lvalues и rvalues не очень хорошо (я просмотрел дюжину книг, и не мог найти ни одно понравившееся мне объяснение). Причина может быть в том, что нет последовательного определения lvalue и rvalue даже среди языковых стандартов. Спецификация 1999 C Standard определяет lvalue не так, как спецификация 1989 C Standard, и каждая из них отличается от C++ Standard. Причем ни один из стандартов не дает четкого определения. Учитывая неоднозначность в определениях для lvalue и rvalue среди языковых стандартов, я не подготовлен предложить точные определения." Но вы не волнуйтесь, это же всего-навсего правое и левое значение! Вам что, с "право" и "лево" сложно разобраться?
И вот проходит время, и нам говорят: знаете что, с lvalue и rvalue неувязочка вышла. Мы тут поверх всего накрутили семантику перемещений, кстати, она тоже будет влиять на оптимизации, ну, и в общем, добавили glvalue и prvalue. Как-нибудь сами разберитесь что это такое. Тока смотрите, помимо аллокации у нас теперь появляется материализация! А, да, бонусом ловите еще и xvalue. Это такая сущность, которая как бы существует, но она временная. То есть она как бы есть, но ее нет. Ну вот так работают точные науки, мы же программисты.
Так что вот вам инструментарий, пользуйтесь. Кто употребил в одном предложении "плюсы" и "непостижимо"? Вы неправы! Мы же пользуемся C++! И другие смогут!
C++ - неизучаемый язык
В этом контексте всегда вспоминаю про эту страницу: C++ compiler support
C++ - это тот случай, когда создатели не хотят или не способны контролировать сложность инструмента и перекладывают кучу проблем на голову пользователей. Чтобы написать быструю, надёжную программу, программист должен знать и помнить о тысяче нюансов, как работает компилятор в каком стандарте, как и когда он делает оптимизации и т.д. И чем больше кодовая база тем сложнее контролировать и учитывать всё это в проекте. Хуже всего то, что язык молча позволяет написать неправильно даже тогда, когда можно было бы об этом сообщить или просто не дать этого сделать. Написание программ на C++ - это хождение по минному полю с постоянно возрастающей когнитивной нагрузкой. Даже если ты профессиональный сапёр, приятного в этом мало. Эта статья хороший тому пример.
Написание программ на C++ - это хождение по минному полю с постоянно возрастающей когнитивной нагрузкой. Даже если ты профессиональный сапёр, приятного в этом мало
Ну, я профессиональный сапёр, и мне нравится :) Тем более, что на современном С++ писать на порядок приятней, чем на С++ времён до C++11
согласен, казалось пойдём простым путём и будем создавать легкие конструкции структур с только деструкторами и где-то на 1500 строке понимаешь что структура данных в голове как на дискете - инициализация
на помощ приходит RAII, smartpointers, OOP и понеслось )
Было бы намного лучше если бы была ошибка при попытке вызова std::move для const переменной
Любопытно, что С++ позволяет сделать перегрузку метода для случая const &&, что-то вроде:
struct demo {
void f() const && { ... }
...
};
И вот если есть такая перегрузка, то std::move для константного объекта может иметь смысл.
Простая демонстрация: https://godbolt.org/z/MW8dMKvvK
Где это может пригодиться на практике лучше не спрашивать :)
Но с точки зрения возможности получить const && у std::move для константного объекта есть своя логика.
Во всей стандартной библиотеке C++ эта функция, пожалуй, названа наиболее превратно.
Как и в асэмблере, где она не перемещает, а копирует. Што ж так не везёт этому слову...
Перевод ужасен, не (грамотный) программист переводил, пара моментов:
1. "Both x and name are lvalues. " переведено как "и x, и name являются левыми значениями", ненадо переводить термин lvalues
2. "they stick around beyond the current expression" переведено как "и они могут постоянно находиться за пределами текущего выражения", кто на ком стоял ?
Далее не читал перевод, перешел на огригинал
std::move ничего никуда не двигает: подробный рассказ о категориях значений в C++