Pull to refresh

Comments 69

А мне больше понравилось другое объяснение, ещё более простое:


Есть в C++ два типа ссылок: обычная & и rvalue &&. Если объект передаётся по rvalue-ссылке, то это значит, что вызывающий код отказывается от владения объектом, и функция может делать с ним всё, что ей хочется. Ну а с помощью std::move мы сообщаем об этом намерении явно.

Я попытался проследить, откуда взялось вообще понятие lvalue и rvalue - оно тянется в глубь веков, ещё до BCPL. И, если честно, определения я так и не смог найти. :-(

Поэтому всё равно какое-то кол-во магии остаётся.

Value categories

Там вполне чётко написано, что lvalue может находиться слева от оператора присваивания, а rvalue - справа.

Хотя эту ссылку вы должны были найти раз добрались до BCPL.

Слишком сложно написано, для новичка это все равно, что текст на незнакомом языке.
Плюс эти правила постоянно меняются: RVO, copy elision, нестандартные оптимизации компилятора.
Лучше понимать общий принцип.

Понятия lvalue и rvalue взялись как раз понятно откуда.


lvalue — это то, что может стоять слева от знака присваивания
rvalue — это то, что может стоять только справа от знака присваивания

const int a = 2;
a = 3; // No
const int b = a; // Yes

Получается, a — это rvalue?

Нет, а - это const lvalue. Ты же использовал a слева строчкой выше и навесил квалификатор const, дальше уже он не разрешает a находиться слева по семантическим правилам. Есть ещё одно часто используемое упрощённое определение: lvalue - это то, от чего можно взять адрес (&a).

Ты же использовал a слева строчкой выше

Не вопрос, меняю присваивание на явный вызов конструктора:


const int a { 2 };
a = 3; // No
const int b = a; // Yes

Есть ещё одно часто используемое упрощённое определение: lvalue — это то, от чего можно взять адрес (&a).

Ну вот смотрите, как я беру адрес от rvalue:



class A {};

A* get_addr(A&& arg)
{
  return &arg;
}

int main(int argc, char **argv)
{
    auto addr = get_addr(A{});
    return 0;
}

При передаче аргумента значение rvalue становится lvalue, т.к. связано с переменной. Ну а дальше спокойно берём указатель и даже не видим предупреждения.

`const int a { 2 }` - это в точности `const int a = 2` - инициализация нового (ранее не инициализированного) объекта.

> Ну вот смотрите, как я беру адрес от rvalue: `A* get_addr(A&& arg) { return &arg; }`

Это частая ошибка. Аргументы функции - всегда lvalue. В данном коде это ссылка на rvalue. Конечно можно взять адрес lvalue. Ты берёшь адрес переменной, а не значения rvalue. Это как `int a = 1; int* b = &a;`, сравни `int a = 1; int* b = &(a + 1);`.

const int a { 2 } — это в точности const int a = 2 — инициализация нового (ранее не инициализированного) объекта.

Так, речь шла о присваивании, а тут его нет в явном виде.
Но да, такое принимается.


Ты берёшь адрес переменной

Так покажите мне переменную, от которой я беру адрес. Напоминаю, что оператор & здесь берёт не адрес переменной arg, а адрес значения, на которую ссылка ссылается.

arg - это локальная переменная фунции. &arg - это адрес переменной arg. Нет такого "адрес значения", оно может быть числом в регистре процессора?

оно может быть числом в регистре процессора?

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

Вы сейчас путаете языковую модель и последствия оптимизации компилятором.

Ваше объяснение некорректно - как минимум, у вас куда-то константные ссылки потерялись... Во многом ведь из-за них весь сыр-бор с std::forward(), коллапсом ссылок в шаблонах и прочими ещё более странными наворотами.

Кроме того, из вашего объяснения можно получить неверное представление, что якобы из обычной ссылки нельзя сделать rvalue-ссылку (ведь вызывающий код не отказывался от владения, так? однако сделать std::move() в функции никто не запретит, так что получится, что как бы можно "заставить" вызывающий код отказаться от владения?). И вообще, даже с lvalue-ссылкой функция может сделать с объектом что угодно.

UFO just landed and posted this here
Не знаю почему, но мне все эти ссылки (и перемещаемые, и обычные) интуитивно не нравятся. Не проще ли явно передавать указатель (который всегда 4 или 8 байт) и делать с объектом что угодно по указателю?
Я увлекаюсь дизайном языков программирования, и вот как раз недавно собирал информацию по ссылкам. Ссылки (обычные) появились в С++ для того, чтобы можно было возвращать из функции что-то, что может быть слева от присваивания (LValue). Это нужно для перегрузки операторов =, *, [], ++, --. Заодно появилась возможность писать что-то вроде foo() = 42.
В Rust таких неявных ссылок нет (там понятие «ссылка» есть, но это скорее просто разновидность указателя с явным разыменованием). Перегрузка lvalue-операторов есть, возвращают они ссылки, требующие явного разыменования, и тем ни менее как-то выкрутились. Например перегрузка индексации.
Передача аргументов по ссылке в С++ происходит неявно, что не очень хорошо с точки зрения наглядности. В C# используется слово ref для явной передачи по ссылке, и читаемость такого кода значительно лучше. Кстати на Хабре есть статья, в которой перечисляются и другие недостатки…
Ну и move-семантика. Ее назначение понятно, но вот реализация… чисто синтаксически это выглядит как-то криво и громоздко. std::move которая на самом деле ничего не делает, а просто пытается изменить тип аргумента. А есть еще std::forward. Все это производит впечатление какой-то кривизны…
Интересно, можно ли сделать лучше? Пусть не в рамках С++, а вообще?

При том, что деконструкция в Rust выворачивает мозг в процессе обучения, после Rust семантика С++ выглядит как общение с пациентом ПНД - он что-то хочет сказать, иногда это осмысленно, но так коряво и ужасно (а иногда и не осмысленно), что просто ой.

Rust тоже сложный и навороченный, там есть недостатки, хотя за счет того, что он создавался с оглядкой на огромный опыт С++, там больше продуманности.
Вообще мне хочется придумать идеальный синтаксис и семантику для ссылок всех видов. Но пока не получается…

Сравнивая С++ и Rust, Rust - это C++ из которого убрали "С". Стало резко проще и логичнее. При том, что нюансы тонкие и требуют внимания, они больше не ведут к "упс, ну тут дефолт из-за совместимости со старыми версиями, которые совместимы со старыми версиями, которые до сих пор не знают какого размера int".

Rust до сих пор не научился в placement new. Как следствие, нельзя создать большой объект сразу в куче, он обязательно будет создан сначала на стеке (который при желании можно даже переполнить) и скопирован. Вот когда завезут placement new, тогда и поговорим. А пока это царство memcpy.

А вот это, кстати, интересный поинт. Я никогда не думал про такой вопрос. В целом, у Box, всё, что uninit помечено как nightly... Я не знаю, этого достаточно или ещё что-то нужно?

Если в плюсах сделать new MyClass(...), то объект инициализируется сразу в куче. Окей, в безопасном Rust нету прямого аналога new, скорее корректно говорить про то, что Box похож на unique_ptr из STL. В STL есть make_unique, который опять же сначала выделит память в куче, а затем уже проведёт инициализацию объекта сразу в куче.

В Rust ты можешь без unsafe и сторнних либ, обрабатывающих только частные случаи, сделать только Box::new(MyClass::new(...)). При этом объект обязательно сначала создасться на стеке, а потом скопируется в кучу. Обычно это просто замусоривает код вызовами memcpy под капотом (копирование со стека на стек компилятор ещё умудряется оптимизировать обычно, но вот с Box он бессилен, проблема более фундаментальная, потому что вызов MyClass:new происходит раньше вызова Box::new и тут одним тюнингом оптимизатора без лютых костылей не справиться), но если MyClass очень большой (например, внутри него содержится массив крупной размерности и при этом мы не хотим делать лишний indirection выделяя его отдельно, к тому же это сильно ухудшает выразительность языка, потому что из стандартной библиотеки динамически аллоцированному массиву соответствует Vec, а он имеет переменную длинну и мы лишаемся гарантий размера массива в compile-time, аналога array из STL опять же не завезли в стандартную библиотеку)., то можно и переполнение стека словить.

Также placement new нужен ещё для реализации кастомных аллокаторов (например, арена объектов), в embedded и т. д.

При этом сам по себе placement new не выглядит как что-то принципиально unsafe, при отсутствии других unsafe-вызовов, его корректность отлично можно провалидировать.

Ну вот и выходит, что из-за его отсутствия zero cost abstractions перестают быть zero cost. Runtime проверку границ массивов и прочие assert-ы штатными опциями компиляции отключить можно (и для релизной сборки чего-то критичного к скорости именно так и делают), а заставить оптимизировать placement new нельзя. В итоге целое подмножество алгоритмов и структур данных на Rust становятся by design менее эффективными, чем на C/C++, хотя сами алгоритмы никакими хаками на грани undefined behavior не являются.

из стандартной библиотеки динамически аллоцированному массиву соответствует Vec, а он имеет переменную длинну и мы лишаемся гарантий размера массива в compile-time

Можно же Box на массив фиксированной длины делать.

При этом сам по себе placement new не выглядит как что-то принципиально unsafe

А это и есть принципиально unsafe, конструктор получает на руки неинициализированный кусок памяти и должен каким-то образом установить значения этих полей. То у нас есть неловкий промежуток времени, когда объект вроде бы и есть, но что-то делать с ним нельзя. В частности, нельзя вызывать никакие методы, потому что они могут прочитать неинициализированные значения.


Впрочем, про ситуацию с конструкторами уже всё расписал Кладов

Да, в Rust нет конструкторов, MyClass::new просто возвращает сконструированный экземпляр реализуя что-то вроде паттерна фабрика. Соответственно, инициализация происходит атомарно и как бы safe.

Но, насколько я помню из ABI C/C++ (и я очень сомневаюсь, что в Rust что-то отличается), функция возвращающая структуру на самом деле неявно просто принимает указатель на область памяти, куда нужно положить результат, а эта область памяти выделяется вызывающей функцией, по той банальной причине, что через регистры произвольную структуру вернуть невозможно, они сильно ограничены по размеру и количеству.

Соответственно, чисто технически ничто не мешает передать этой самой функции динамически выделенную память вместо статически выделенной и избежать аллокации на стеке.

Собственно, проверил: https://godbolt.org/z/676jozzxq

Можно обратить внимание, что, конечно, функция конструирует объект в своей локальной стековой области, но в конце вызывает memcpy, который копирует результат инициализации по адресу, переданный в RDI (он сохраняется в стек вначале функции, потому что перезаписывается вызовом memset, а потом восстанавливается к исходному значению) вызывающей функцией. То есть память была выделена вызывающей функцией. И ничего бы не случилось, если бы в RDI вместо адреса со стека пришёл бы адрес из кучи.

Теперь включаем оптимизации https://godbolt.org/z/GvWTYPhqc и всё получается вообще красиво - memcpy исчезает, объект инициализируется inplace, прямо по тому адресу, который был передан вызывающей функцией.

То есть на уровне ABI любой new в Rust вполне себе placement, как и в C/C++.

Вопрос в том, можем ли мы как-то контролировать, что Rust передаст при вызове функции в качестве этого адреса буфера. Вполне себе можем - https://godbolt.org/z/oY5oGhY51

Копирования и аллокации на стеке не произошло, но нам пришлось обратиться к unsafe и сделать в три шага то, что по сути реализовано внутри Box::new.

То есть проблема не в самом языке, а в стандартной библиотеке Rust, которая не содержит аналога std::make_unique из C++, которая бы обеспечивала вызов конструктора (или функции возвращающей сконструированный объект - как мы выяснили выше, на уровне ABI они ничем от конструкторов не отличаются) после аллокации памяти, а не до неё, чтобы оптимизатор смог убрать копирование и лишнюю аллокацию на стеке. Текущий вариант обязывает сначала вызвать конструктор, сохранить результат на стеке вызывающей функции, а уже затем вызвать Box::new передав этот самый результат в качестве аргумента. А нужен макрос (в C++ make_unique является шаблоном), который бы унёс реальный вызов конструктора внутрь Box::new или что-то такое (при этом, насколько я помню, макросы в Rust совершеннее плюсовых шаблонов в том плане, что не будет проблем из-за прав доступа к "методу-конструктору", в плюсах make_unique не работает с приватными конструкторами).

Проблема в том, что для C++ make_unique является идеоматическим способом аллокации с "умными указателями". За std::unique_ptr(MyClass(...)) будут очень косо смотреть. А в Rust именно Box::new(MyClass::new(...)) является идеоматическим способом создания объекта в куче и уже огромные тонные кода написаны именно так, потому что авторы стандартной библиотеки не продумали этот ньюанс (ну или хотели "как угодно, лишь бы не как в C++"). Вторая плохая новость в том, что в Rust вызов макросов принципиально отличается от вызова функций, то есть по-тихому переписать Box::new как макрос без потери совместимости со старом кодом нельзя.

Конечно, теоретически у авторов Rust есть альтернатива - вставить костыль в компилятор, который бы переупорядочивал аллокацию и вызов конструктора. В целом они вполне могут это сделать, Box::new и так работает на "магии" через ключевое слово box, которые скрывает всю реализацию в недрах компилятора.

P. S.: Все эти рассуждения неприменимы для объектов размером 10-20 байт, потому что они умещаются в регистры и аллокаций на стеке принципиально не происходит, да и ABI передачи результата функций получается через регистры, а не через предаллоцированную область памяти. С такими объектами никаких проблем нет, потому что и риска переполнения стека они не несут, и их копирование не несёт оверхеда (потому что доступ к регистрам гораздо дешевле доступа к памяти).

P. P. S.: Проблема в том, что в Rust программисту надо постоянно держать в уме сколько байт будет весить объект и при необходимости переходить на аллокацию в 3 строчки (две из которые unsafe) вместо 1. Либо аллоцировать вообще всё этим способом (минусов этот способ, кроме многословности не имеет, unsafe лишь значит, что компилятор не может проверить корректность автоматически, но конкретно эти три строчки абсолютно безопасны, более того, внутри стандартной библиотеки вполне себе встречается unsafe), либо объявить свой макрос "Box::new здорового человека" (но динамическая аллокация объектов в куче - базовый функционал любого языка, у которого в принципе есть куча, и это очень странно, если программист должен писать свой велосипед в этой ситуации, тем более что в Rust изначально "батареек" гораздо больше, чем в плюсах).

У меня чем дальше, тем чаще возникает вопрос. А так ли уж важно иметь явную inplace инициализацию везде? В большей части прикладного кода в общем-то по барабану, будет там лишний memcpy на пару сотен байт или нет. RVO в релизе прекрасно работает и покрывает 95% случаев. Ну а если прям вот совсем нужен inplace init, можно в отдельных местах unsafe.
Ещё следует учесть, что поддержку оператора box нужно будет делать не только для типа Box, но везде, где происходит выделение не на стеке.

Достаточно все функции аллокации не на стеке делать через макросы. Как в плюсах есть шаблоны make_unique, make_shared и т. д. Чтобы вызов конструктора/функции инициализации смог выполниться позднее выделения памяти. Сейчас же стандартная библиотека Rust генерирует код аллокации принципиально неоптимизируемый, ради чего?

Никакой безопасности или выразительности это не даёт (если вставить потроха Box::new в виде вызова оператора box, можно обойтись без unsafe, unsafe версия на nightly функциях тоже unsafe только для компилятора, для человека очевидно, что всё безопасно). Просто разработчики не учли кейс изначально (или очень боялись передрать паттерн из плюсов), а сейчас у них затруднительная ситуация - либо вставлять костыль в компилятор, либо ломать обратную совместимость (Box::new нельзя тихо и незаметно сделать макросом, потому что у них другой синтаксис вызова). Действительно, потери для 95% случаев незначительны, но это не zero cost abstractions, и не оверхед ради безопасности, это просто просчёт дизайна одной из базовых функций языка (выделение памяти в куче).

Вот когда такие детские болезни (а это именно оно - потеря производительности без какого-либо выигрыша) исправят (красиво или хотя бы костылями), тогда и поговорим.

Сейчас же стандартная библиотека Rust генерирует код аллокации принципиально неоптимизируемый, ради чего?

Как вы сами показали, компилятор вполне способен такое оптимизировать.


Просто разработчики не учли кейс изначально

Насколько мне известно, оператор box в каких-то вариантах существует с незапамятных времён. В целом, дискуссия по placement box активно велась ЕМНИП года два или три назад.


или очень боялись передрать паттерн из плюсов

Дело скорее в отсутствии концепции конструктора. В таком случае любой случай allocate then init становится unsafe.


Последнее, что нашлось — Placement by return, эксплуатирующий (Named) Return Value Optimization и Guaranteed Copy Elision. С моей точки зрения, это более элегантный подход, не требующий специального синтаксиса. Единственная проблема — RFC висит уже больше года. Но обсуждение идёт, последний комментарий был 25 дней назад.

И что значит незначительная просадка? Это почти удвоение времени инициализации всех структур с тривиальными конструкторами, если они создаются где угодно, кроме стека. То есть все комплексные структуры данных типа деревьев, не требующие интенсивных вычислений для своего создания получают штраф к производительности просто так.

И что значит незначительная просадка? Это почти удвоение

По сравнению с затратами на саму аллокацию. Плюс вы сами привели примеры, что компилятор вполне способен "прожевать" не слишком сложные случаи и выполнить RVO.

Кстати, можно обойтись без unsafe, если использовать магический оператор box напрямую: https://godbolt.org/z/73d44dPMs. Однако его стабилизация под огромным вопросом.

Верно, проблема ссылок в их неявности. В C#, например, о намерении передать аргумент по ссылке, допускающей изменение объекта, надо указывать не только в заголовке функции, но и каждый раз при вызове функции.

С появлением in всё стало немного сложнее :)
Так как реализация это просто передача по ссылке и дополнительный атрибут, C++/CLI его попросту игнорирует и позволяет изменить объект неявно.

class Program
{
    public static void Main()
    {
        int i = 2;
        new A().F(i);

        Console.WriteLine(i); // 1
    }
}
public ref class A
{
public:
	A(){}
	void F([System::Runtime::CompilerServices::IsReadOnlyAttribute] int% a)
	{
		a = 1; // OK
	}
};

Ещё одно применение для P/Invoke, чтобы не писать ref.
Следует использовать с умом, не стоит преподносить сюрпризов лишний раз :)

[DllImport("abc")]
public static extern F(in POINT pt);
С появлением in всё стало немного сложнее :)

Под капотом — да. Но если не вдаваться в подробности и не стрелять себе в ногу PInvoke, System.Runtime.CompilerServices.Unsafe. и генерацией IL-кода в райтайме, но никаких проблем не будет. Максимум — просадки в производительности из-за копирования структур.

У меня в примере нет ничего из вышеперечисленного.

Всего лишь язык неподдерживающий «in» также как C#.

Ну так у вас код не на C#, а на мёртворождённом Managed C++.

Это C++/CLI, а учитывая поддержку .NET 6, немного рано говорить про смерть :)

Это C++/CLI,

Странно. Мы всегда его называли Managed C++.


а учитывая поддержку .NET 6, немного рано говорить про смерть :)

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

Managed C++ — это там где ключевое слово __gc используется.
А у C++/CLI синтаксис не такой страшный.

А зачем MS ввели в плюсы процентные ссылки вместо вполне себе существовавших обычных ссылок (&) ? Заодно никаких выстрелов в ногу, потому что in был бы const & без всяких дополнительных аннотаций и компилятор запретил бы присваивание.

Чтобы различить ссылки на управляемую и неуправляемую память.
Но вообще, Managed C++ — мёртвый язык.

да, в C++ чёрт ногу сломит.
Там ещё есть правила сворачивания ссылок (когда левая ссылка всегда «побеждает»), понятие универсальной ссылки…

А std::forward фактически это вот такой не C++ код (но более понятный :) )
template <typename Fun, typename Arg> decltype(auto)
bar(Fun fun, Arg&& arg)
{
   if(arg is rvalue)
      return fun(move(arg));
   else
      return fun(arg)
}
, что эквивалентно
template <typename Fun, typename Arg> decltype(auto)
bar(Fun fun, Arg&& arg)
{
   return fun(std::forward<Arg>(arg))
}

т.е. здесь игра как раз идёт на том, что ссылка по правилу сворачивания может оказаться rvalue или rvalue. Это т.н. perfect forwarding.
Мне вот интересно — это всё следствие введения в язык концепции ссылок как таковой? Можно ли было этого избежать, если изначально сделать как-то по другому, и если да то как?

Это всё следствия инкрементального усложнения языка, когда в язык добавляли новый функционал, но сохраняя обратную совместимость.

Это понятно:) Интересно — а можно ли в принципе реализовать все то, что реализовано в С++, без сохранения обратной совместимости, но и без костылей?

Можно, но язык, скорее всего, повторит участь D.
Проблема C++ в том, что он очень сложный и имеет высокий порог вхождения. Поэтому взлетают более простые языки типа Rust и Go.

Можно, но язык, скорее всего, повторит участь D.
Интересно как именно.
У меня ведь чисто академический, а не коммерческий интерес. Мне достаточно знать, как именно могла бы быть устроена идеальная модель ссылок, чтобы через нее понимать преимущества и недостатки реальных моделей в реальных языках. Хочется видеть систему, а пока я вижу какую-то мешанину — и в С++, и не только в нем.

Проблема заключается в том, что выживают только языки, к которым имеется коммерческий интерес. А идеальные языки представляют интерес исключительно академический. Как в анекдоте про шашечки или ехать.


Как по мне, так жизненный цикл всех популярных языков идёт по схеме:


  1. Стадия хайпа. Кто-то решает создать новый крутой язык, который решит все проблемы современности. При этом язык должен быть простым и иметь низкий порог вхождения, но одновременно решать одну из наиболее острых проблем с другими языками.


  2. Стадия роста. Язык быстро набирает популярность. Программисты набираются опыта, и функционала языка им начинает не хватать. Это приводит к закономерному развитию языка: разрастанию функционала, добавлению языковых конструкций.


  3. Стадия угасания. По мере развития языка возрастает его сложность и повышается порог вхождения — это приводит к снижению популярности языка у начинающих. Также снижается гибкость языка из-за груза legacy, всё сложнее становится добавлять функционал и реагировать на потребности. Язык теряет популярность и у сеньоров.



Далее возвращаемся к пункту 1: появляется новый язык ...

Да я не против, согласен с вами. Но мы говорим о разных вещах.
У меня чисто академический интерес: как должна выглядеть идеальная система ссылок в идеальном языке программирования, на котором было бы идеально удобно писать программы, который бы объединял достоинства и исключал недостатки существующих реализаций. Есть такой язык в природе или его нет и никогда не будет — мне не важно.

Вы, кстати, ошиблись. Здесь arg — это всегда lvalue, а вот тип arg может быть как lvalue reference, так и rvalue reference. Собственно, из-за этого и нужен костыль в виде std::forward.

Пусть не в рамках С++, а вообще?

Конечно можно. В Rust лучше сделано. Но это всё добавляли уже когда была куча кода написана.

Вот тут есть статья, объясняющая почему и как перемещение сделано в C++ и в Rust.

С указателями есть пачка своих граблей, особенно с точки зрения читаемости кода, т.к. они почти всегда гораздо гибче и иногда эта гибкость не нужна:

  • указатель может быть равен nullptr. Но не всегда, есть код где nullptr никогда не будет присвоен указателю. Глядя на сигнатуру функции не понятно, обрабатывает ли она nullptr, а глядя на параметр внутри функции не понятно, могут ли в него передавать nullptr.

  • семантика владения: указатель может отражать владение объектом (а значит, кто-то должен потом освободить ресурс, на который он указывает), а может и не владеть. Ссылка не владеет тем, на что указывает, за редкими исключениями.

  • указатель может быть изменен, другими словами, то, что указатель был инициализирован в конструкторе, не гарантирует что ниже по коду его никто не сменил. Есть константные указатели на не-константу, конечно, но не все соблюдают const-correctness настолько хорошо.

Кроме того, указатель - это отдельная сущность, N байт адреса того, на что указываем. И взятие указателя лишает нас информации о размере массива, что бывает важно и удобно при передаче массива в шаблонный метод:

uint8_t arr[3];        // sizeof() == 3
uint8_t* parr = arr;   // sizeof() == sizeof(uint8_t*)
auto& ref = arr;       // sizeof(ref) == 3

Lifetime extension для указателей на локальную переменную не изобрели. Для константных и rvalue-ссылок этим, наоборот, можно пользоваться. Хотя вопрос стиля, конечно, спорный.

Ссылка тоже всегда 4 или 8 байт, а нужна она потому что гарантирует неравенство nullptr. Тоесть если вы передаёте указатель вы обязательно должны добавлять эту проверку перед разименоввыванием

В конечном счете, на уровне ассемблера нет вообще никаких ссылок. Все делается через указатели (адреса). И если объект не нужен, просто отдаем его адрес в памяти.

Ссылочный механизм создал дополнительный уровень сложности, что породило семантику перемещения которую как не объясняй - недосказанность остается. А ведь все очень просто: единственная реальность это указатель.

Ну это вы слишком упростили, это другой уровень абстракции. Под капотом может никакого указателя и не быть, его может вымыть оптимизатором.

Мне кажется, что проще всего объяснить, что ссылка это тот же самый указатель, только после инициализации все обращения к p превращаются в обращения к *p (из-за этого ссылку уже нельзя переприсвоить, потому что присваивание произойдёт над значением, на которое она ссылается как если бы мы сделали *p = x). Применение ссылок - более малословная альтернатива указателям в ситуациях, когда нам нет необходимости в возможности изменить на какой объект ссылается указатель. Этого знания достаточно для 90% применений ссылок. Остаётся ещё rvalue (&&). Но с ним тоже всё просто - он лишь заставлят выбирать правильные перегрузки функций. А с семантической точки зрения аргумент функции типа && означает, что передавшему объект больше не нужен и его можно "портить" (при условии, что после "порчи" деструктор этого объекта сможет корректно отработать) в интересах вызванной функции (чаще всего, чтобы забрать содержимое объекта без копирования куда-то в другое место, но не обязательно). В целом "портить" можно и любую другую неконстантную ссылку, но более выразительно и правильно делать так именно с rvalue, чтобы не вызывать удивления у коллег (к тому же многие стандартные классы и функции имеют именно такое поведение и удобно быть с ним консистентным).

а выражение "some text" его не имеет, точнее его адрес не так просто найти и оно недолгоживущее.

const char* s = "some text";

printf("адрес, который не так просто найти: %p", s);

И что значит "недолгоживущее"? Строка зашита в бинарь программы, куда ещё дольше жить?

Спасибо за замечание.
Имелось ввиду адрес объекта "some text", т.е. вот так:

cout << &"some text";

Выглядит совсем нелогично, понимаю, но такой пример часто приводят на тематических сайтах.

Про "недолгоживущие" - убрал, действительно вводит в заблуждение.

Новички, узнавшие про move, очень любят его применять по делу и без, например в функции

std::string GetText() {
  std::string s = "Hello, World!";
  return std::move(s);
}

Объяснение простое - move() не копирует объект, он быстрый, поэтому надо вызвать move(), чтобы наверняка!
А плохо здесь то, используя move() для возврата из функции, явно указывается, что должен быть использован конструктор копирования. Но в языке есть такое понятие, как (N)RVO - (Named) Return Value Optimization, когда объект может быть возращен без вызова каких-либо конструкторов.
Поэтому никогда не нужно делать move() на возвращаемое значение.

Ну, новичков можно понять, ведь достаточно малейшего изменения в этом паттерне, чтобы внезапно возникала ненужная копия, и снова требовался бы std::move(). Например, если возвращать не s, а поле x из структуры struct Foo {std::string x;}. И RVO, несмотря на кажущуюся похожесть этого случая, тут не сработает. Всё-таки семантика перемещения в C++ очень неинтуитивна.

Но в языке есть такое понятие, как (N)RVO — (Named) Return Value Optimization, когда объект может быть возвращён без вызова каких-либо конструкторов.

Может, но не должен. То есть на усмотрение компилятора.

> Return value optimization is mandatory and no longer considered as copy elision
C++17.

upd: хм, эта часть все же не про NRVO, похоже, ошибся.
Но ощущение что в С++20 он все же будет обязательный так же.

Где фигурные скобки задают область видимости. При выходе за ее пределы запускаются деструкторы классов для объектов

Стоит отметить, что деструкторы связаны не с областью видимости, а с временем жизни. В данном случае конечно два понятия ("время жизни" и "область видимости") совпадают, но переменная может быть статичной, например, и тогда область видимости будет той же, а вот жить она будет до конца программы и никакой деструктор не вызовется.

Ценный комментарий, спасибо. Добавил в текст пометку

В данном случае конечно два понятия ("время жизни" и "область видимости") совпадают, но переменная может быть статичной, например, и тогда область видимости будет той же, а вот жить она будет до конца программы и никакой деструктор не вызовется.

Вызовется-вызовается. Ишшо как вызовется. Вы только полюбуйтесь сколько кода генерируется, чтобы он вызвался.

Аж прям память в __cxa_atexit аллоцируется, чтобы всё правильно вызвалось.

Имелось в виду, что не вызовется при выходе из области видимости, а не при завершении программы. При завершении конечно вызовется

Vector(size_t size, T value) : data_(size, value) {

Не надо шаблонные параметры передавать по-значению без необходимости. Здесь должно быть

Vector(size_t size, const T &value): data_(size, value) {
Sign up to leave a comment.

Articles