Грокаем C++
9.36K subscribers
43 photos
1 video
3 files
570 links
Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов.

По всем вопросам (+ реклама) @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Мувать не всегда дешево
#новичкам

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

Но мир не такой уж солнечный и приветливый. Это очень опасное...

Если вы придерживаетесь RAII, пользуетесь контейнерами и умными указателями, то вы практически всегда пользуетесь правилом нуля и никогда не определяете самостоятельно специальные методы класса и, в частности, конструктор перемещения и оператор перемещающего присваивания. Компилятор сгенерирует их за вас, ленивых дядь.

Рано или поздно вы немного отрываетесь от "низов": вас уже не интересует КАК конкретно эти методы реализованы. Вы оперируете более высокоуровневыми сущностями и полагаетесь на компилятор.

И вот вы в ситуации, когда у вас есть данные, обернутые в класс, которые легально по контексту кода можно мувнуть или скопировать. Условно говоря, у вас есть функция Process, которая принимает данные по значению, чтобы поддержать оба варианта передачи: копирование и мув:

void Process(Data data);


Что выбрать?

"Конечно мувнуть, это же не долгое копирование, выполнится быстро" - вот к таким не совсем корректным мыслям может привести "оторванность от низов".

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

Но это не так! Перемещение - это поверхностное копирование.

Возьмем простой пример:

struct Data {
int a;
double b;
};

Data obj1{3, 3.14};
Data obj2 = std::move(obj1);


Что будет происходить при перемещении obj1? Копирование a и b.

Чуть сложнее:

struct Data {
std::array<int, 5> arr;
};

Data obj1{.arr = {1, 2, 3, 4, 5}};
Data obj2 = std::move(obj1);


Что будет при перемещении obj1, а значит и arr? Тоже копирование! std::array - это массив, фиксированного размера, расположенный на стеке. Как вы собираетесь его перемещать в другой объект? Под другой объект уже выделена своя память на стеке, вы не можете один кусок стека переместить в другой. Вы можете только скопировать значения.

Можно еще занулить конечно, но это редко происходит из соображений перфоманса.

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

struct Data {
std::string * str;
// member functions for making it work properly
};

Data obj1{.str = new std::string("Hello, World!")};
Data obj2 = std::move(obj1);


obj2 теперь имеет такое же значение указателя str, как и obj1, но сама строка оказалась нетронутой.

Более того. Даже если вы используете std::string, то не всегда мув будет быстрее копирования! Thanks to SSO.

Получается, что никто никуда не течет. Все так же пресловуто копируется, кроме динамических данных под указателями.

Теперь снова актуализируем вопрос: мувать или копировать?

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

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

Think logically. Stay cool.

#cppcore #cpp11
3👍3815🔥6😎5
​​Передача владения
#новичкам

Захотелось совсем немного развить тему предыдущего поста.

В целом, мув семантика она не столько про оптимизацию(для этого есть например rvo/nrvo), сколько про передачу владения объектами. И то, что std::move ничего не мувает(а пытается сделать каст к rvalue reference) хорошо укладывается в эту концепцию. Данные не перемещаются, но вы говорите, что передаете владение этими данными.

void bar(std::vector<int>&& vec) {
// do nothing
}
void foo() {
std::vector<int> vec = {1, 2, 3};
bar(std::move(vec));
}


Здесь мы передаем владение вектором из foo в bar. Заметьте, что bar оперирует правой ссылкой, то есть никакие перемещающие конструкторы не вызывались. Но такая сигнатура говорит о главном: bar ожидает эксклюзивного права владения над этим вектором. Вы должны явно мувнуть объект, чтобы вызвать bar. И не важно, что он дальше bar с этим вектором делает. Может ничего не сделает, а может и использует как-то данные. Но так решил автор кода: вызов bar предполагает передачу ему владения вектором.

Другой пример:

std::vector<int> double_elements(std::vector<int> vec) {
for (auto& elem: vec) {
elem *= 2;
}
return vec;
}

void foo() {
std::vector<int> vec = {1, 2, 3};
{
auto doubled = double_elements(vec);
std::println("{}", doubled);
}
vec.push_back(4);
{
auto doubled = double_elements(std::move(vec));
std::println("{}", doubled);
}
}


Функция double_elements принимает вектор по значению и возвращает набор из удвоенных элементов.

Функция foo 2 раза вызывает удвоение значений элементов. По логике функции foo, ей еще нужен vec в целости и сохранности(нужно доложить в него элемент). Поэтому она и передает в первый раз vec в double_elements по значению. Но после второго вызова вектор ей больше не нужен. Поэтому можно передать владение им в double_elements: возможно он им распорядится лучше.

Еще одна вещь, которая подчеркивает передачу владения: moved-from объект практически никак в общем случае нельзя безопасно использовать, кроме как безопасно разрушить или переприсвоить(в комментах под прошлым постом более конкретно обсуждали этот момент). Даже если функция принимает rvalue reference, это не значит, что она не изменяет объект: возможно внутренние вызовы это делают.

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

Give away what you don't need. Stay cool.

#cppcore #cpp11
18👍12🔥6
Атрибуты везде
#опытным

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

Есть на самом деле 3 легальных места для навешивания атрибутов на функцию.

1️⃣ Перед типом возвращаемого значения:

[[deprecated]] int foo() { return 42; }


Тогда он работает при непосредственном использовании функции.

foo();
// warning: 'int foo()' is deprecated


2️⃣ После имени функции:

int foo [[deprecated]] () { return 42; }


В таком виде атрибут тоже применяется к самой функции.

3️⃣ После параметров:

int foo() [[gnu::deprecated]] { return 42; }


Тогда атрибут применяется к типу функции, а не к самой функции. Разница вот в чем:

int foo() [[gnu::deprecated]] { return 42; }

int main() {
foo(); // no warnings
using FuncType = decltype(foo); // use of type is deprecated
}


Обычный вызов функции прекрасно компилируется. Но вот использование типа функции через decltype помечается как устаревшее.

Причем gcc и clang по-разному интерпретируют эту ситуацию. Clang говорит, что gnu::deprecated нельзя применять к типам и игнорирует атрибут. Вот ссылка на годболт для интересующихся.

Соответственно, в лямбде в тех же местах можно ставить атрибуты:

auto complicated_compute = [] [[nodiscard]] () [[gnu::deprecated]] {
return 2 * 2;
};


Признавайтесь, знали?)

Have your own opinion. Stay cool.

#cppcore
👍18🤯188🔥5