Грокаем 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
Недостатки std::make_shared. Непубличные конструкторы
#новичкам

std::make_shared - это сторонний код по отношению к классу, объект которого он пытается создать. Поэтому на принципиальную возможность создания объекта влияет спецификатор видимости конструктора.

Если конструктор публичный - проблем нет, просто вызываем std::make_shared.

Но things get trickier, если конструктор непубличный. Его тогда не может вызывать никакой чужой код.

Для определенности примем, что конструктор приватный. Это может делаться по разным причинам. Например у нас есть необходимость в создании копий std::shared_ptr, в котором находится исходный объект this. Тогда класс надо унаследовать от std::enable_shared_from_this и возвращать из фабрики std::shared_ptr. Если создавать объект любым другим путем, то будет ub. Поэтому, как заботливые нянки, помогает пользователям не изменять количество отверстий в ногах:

struct Class: public std::enable_shared_from_this<Class> {
static std::shared_ptr<Class> Create() {
// return std::make_shared<Class>(); // It will fail.
return std::shared_ptr<Class>(new Class);
}
private:
Class() {}
};


Использовать make_shared здесь не выйдет. Хоть мы и используем эту функцию внутри метода класса, это не позволяет ей получить доступ к приватным членам.

В таком случае мы просто вынуждены использовать явный конструктор shared_ptr и явный вызов new.

А как вы знаете, в этом случае будут 2 аллокации: для самого объекта и для контрольного блока. Если объект создается часто, то это может быть проблемой, если вы упарываетесь по перфу.

Hide your secrets. Stay cool.

#cppcore #cpp11
614👍14🔥61
​​Недостатки std::make_shared. Кастомные делитеры
#новичкам

Заходим на cppreference и видим там такие слова:

This function may be used as an alternative to std::shared_ptr<T>(new T(args...)).

Также видим ее сигнатуру:

template< class T, class... Args >
shared_ptr<T> make_shared( Args&&... args );


И понимаем, что make_shared не предоставляет возможности указывать кастомный делитер. Все аргументы функции просто перенаправляются в конструктор шареного указателя.

Опустим рассуждения об оправданных кейсах применения кастомных делитеров для шареных указателей. Можете рассказать о своих примерах из практики в комментариях.

Мы же попытаемся ответить на вопрос: "А почему нельзя указать делитер?".

Одной из особенностей make_shared является то, что она аллоцирует единый отрезок памяти и под объект, и под контрольный блок. И использует базовый оператор new для этого.

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

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

Да и скорее всего, если вас устраивает помещать объект в шареный указатель вызовом дефолтного new, то устроит и использование дефолтного delete. Поэтому эта проблема тесно связана с проблемой из первой части серии, но не добавляет особых проблем сверх этого.

Customize your solutions. Stay cool.

#cppcore #cpp11 #memory
🔥177👍62❤‍🔥1
​​Какой день будет через месяц?
#новичкам

Работа со временем в стандартных плюсах - боль. Долгое время ее вообще не было. chrono появилась так-то в С++11. Но и даже с ее появлением жить стало лишь немногим легче.

Например, простая задача: "Прибавить к текущей дате 1 месяц".

В С++11 у нас есть только часы и точки на временной линии. Тут просто дату-то получить сложно. Есть конечно сишная std::localtime, можно мапулировать отдельными полями std::tm(днями, минутами и тд), но придется конвертировать сишные структуры времени в плюсовые, да и можно нарваться на трудноотловимые ошибки, если попытаться увеличить на 1 месяц 30 января.

Как прибавить к дате месяц? +30 дней или +1 месяц не канает. А если февраль? А если високосный год?

В общем стандартного решения нет... Или есть?

В С++20 в библиотеку chrono завезли кучу полезностей. А в частности функционал календаря. Теперь мы можем манипулировать отдельно датами и безопасно их изменять.

Например, чтобы получить сегодняшнюю дату и красиво ее вывести на консоль достаточно сделать следующее:

std::chrono::year_month_day current_date =
std::chrono::floor<std::chrono::days>(
std::chrono::system_clock::now());
std::cout << "Today is: " << current_date << '\n';
// Today is: 2025-09-10


Появился прекрасный класс std::chrono::year_month_day, который отражает конкретно дату. И его объекты замечательно сериализуются в поток.

Если вам нужно задать определенный формат отображения - не проблема! Есть std::format:

std::cout << "Custom: "
<< std::format("{:%d.%m.%Y}", current_date)
<< '\n';
// Custom: 10.09.2025


С помощью std::chrono::year_month_day можно удобно манипулировать датами и, главное, делать это безопасно. Что будет если я к 29 января прибавлю месяц?

auto date = std::chrono::year_month_day{
std::chrono::year(2004), std::chrono::month(1), std::chrono::day(29)};
std::cout << "Date: " << date << "\n";

std::chrono::year_month_day next_month = date + std::chrono::months{1};
std::chrono::year_month_day next_year_plus_month =
date + std::chrono::years{1} + std::chrono::months{1};

std::cout << "Next month: " << next_month << "\n";
std::cout << "Next year plus month: " << next_year_plus_month << "\n";

// OUTPUT:
// Date: 2004-01-29
// Next month: 2004-02-29
// Next year plus month: 2005-02-29 is not a valid date


А будет все хорошо, если год високосный. Но если нет, то библиотека нам явно об этом скажет.

В общем, крутой фиче-сет, сильно облегчает работу со стандартными временными точками.

Take your time. Stay cool.

#cpp11 #cpp20
22👍17🔥6😁3
Идиома IILE
#опытным

Неплохой практикой написания кода является определение переменных, которые не изменяются, как const. Это позволяет коду был более экспрессивным, явным, а также компилятор в этом случае может чуть лучше рассуждать о коде и оптимизировать. И это не требует ничего сложного:

const int myParam = inputParam * 10 + 5;
// or
const int myParam = bCondition ? inputParam * 2 : inputParam + 10;


Но что делать, если переменная по сути своей константа, но у нее громоздская инициализация на несколько строк?

int myVariable = 0; // this should be const...

if (bFirstCondition)
myVariable = bSecondCindition ? computeFunc(inputParam) : 0;
else
myVariable = inputParam * 2;

// more code of the current function...
// and we assume 'myVariable` is const now


По-хорошему это уносится в какую-нибудь отдельную функцию. Но тогда теряется контекст и нужно будет прыгать по коду.

Хочется и const сделать, и в отдельную функцию не выносить. Кажется, что на двух стульях не усидишь, но благодаря лямбдам мы можем это сделать!

Есть такая идиома IILE(Immediately Invoked Lambda Expression). Вы определяете лямбду и тут же ее вызываете. И контекст сохраняется, и единовременность инициализации присутствует:

const int myVariable = [&] {
if (bFirstContidion)
return bSecondCondition ? computeFunc(inputParam) : 0;
else
return inputParam * 2;
}(); // call!

Пара лишних символов, зато проблема решена.

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

Тоже не беда! Используем std::invoke:

const int myVariable = std::invoke([&] {
if (bFirstContidion)
return bSecondCondition ? computeFunc(inputParam) : 0;
else
return inputParam * 2;
});


Теперь мы четко и ясно видим, что лямбда вызывается. В таком виде прям кайф.

Эту же технику можно использовать например в списке инициализации конструктора, например, если нужно константное поле определить(его нельзя определять в теле конструктора).

Be expressive. Stay cool.

#cpp11 #cpp17 #goodpractice
🔥48👍2215👎4🤷‍♂3❤‍🔥2
​​Гарантия отсутствия исключений
#новичкам

Переходим к самой сильной гарантии - отсутствие исключений.

В сам язык С++(new, dynamic_cast), и в его стандартную библиотеку в базе встроены исключения. Поэтому писать код без исключений в использованием стандартных инструментов практически невозможно. Вы конечно можете использовать nothrow new и написать свой вариант стандартной библиотеки и других сторонних решений. И кто-то наверняка так делал. Но в этом случае разработка как минимум затянется, а как максимум вы бросите это гиблое дело.

Поэтому повсеместно предоставлять nothow гарантии с использованием стандартных инструментов не всегда реалистично.

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

Но для начала проясним термины.

Под гарантией отсутствия исключений подразумевается обычно 2 понятия: nothrow и nofail.

nothrow подразумевает отсутствие исключений, но не отсутствие ошибок. Говорится, что ошибки репортятся другими средствами(в основном через глобальное состояние, потому что деструктор ничего не возвращает) или полностью скрываются и игнорируются.

Примером сущностей с nothrow гарантией является деструкторы. С С++11 они по-умолчанию помечены noexcept. В основном это сделано для того, чтобы при раскрутке стека не получить double exception.
Но деструкторы могу фейлиться. Просто никаких средств, кроме глобальных переменных для репорта ошибок невозможно использовать. Они ведь ничего не возвращают, а исполняются скрытно от нас(если вы используете RAII конечно).

nofail же подразумевает полное отсутствие ошибок. nofail гарантия ожидается от std::swap, мув-конструкторов классов и других функций с помощью которых достигается строгая гарантия исключений.

Например в swap-идиоме std::swap и мув-конструкторы используются для определения небросающего оператора присваивания.

nofail гарантиями также должны обладать функторы-коллбэки модифицирующих алгоритмов. std::sort не предоставляет никаких гарантий на состояние системы, если компаратор бросит эксепшн.

В языке в целом эти гарантии обеспечиваются ключевым словом noexcept. При появлении этой нотации компилятор понимает, что для этой функций не нужно генерировать дополнительный код, необходимый для обработки исключений. Но у этого есть своя цена: если из noexcept функции вылетит исключение, то сразу же без разговоров вызовется std::terminate.

Provide guarantees. Stay cool.

#cppcore #cpp11
15👍8🔥5❤‍🔥3😁3👎1
​​Недостатки std::make_shared. Деаллокация
#новичкам

Представляете, забыл выложить один важный пост из серии про недостатки std::make_shared. Затерялся он в пучине заметок. Исправляюсь.

В предыдущих сериях:
Кастомный new и delete
Непубличные конструкторы
Кастомные делитеры

А теперь поговорим про деаллокацию.

В этом посте мы рассказали о том, что std::make_shared выделяет один блок памяти под объект и контрольный блок. 1 аллокация вместо 2-х = большая производительность. Однако у монеты всегда две стороны.

Что происходит с объектом и памятью при работе с shared_ptr напрямую через конструктор?


Отдельно выделяется память и конструируется разделяемый объект с помощью явного вызова new, отдельно выделяется память и конструируется контрольный блок.

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

void operator delete(void *ptr) noexcept {
std::cout << "Global delete " << std::endl;
std::free(ptr);
}

class MyClass {
public:
~MyClass() {
std::cout << "Деструктор MyClass вызван.\n";
}
};

int main() {
std::weak_ptr<MyClass> weak;

{
std::shared_ptr<MyClass> shared(new MyClass());
weak = shared;
std::cout
<< "shared_ptr goes out of scope...\n";
} // Here shared is deleting

std::cout << "weak.expired(): " << weak.expired()
<< '\n';
weak.reset();

std::cout << "All memory has been freed!\n";
}
// OUTPUT:
// shared_ptr goes out of scope...
// Dtor MyClass called.
// Global delete
// weak.expired(): 1
// Global delete
// All memory has been freed!


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

Деструктор и delete для разделяемого объекта вызываются ровно в момент выхода объекта shared из своего скоупа. Тем не менее weak_ptr жив, он знает, что объекта уже нет, но своим наличием продлевает время жизни контрольного блока. После ресета weak ожидаемо происходит деаллокация блока.

Что же происходит при использовании std::make_shared? В какой момент освобождается вся выделенная память?

Только когда оба счетчика сильных и слабых ссылок будут равны нулю. То есть не осталось ни одного объекта std::shared_ptr и std::weak_ptr, которые указывают на объект. Тем не менее семантически разделяемый объект уничтожается при разрушении последней сильной ссылки:

void operator delete(void *ptr) noexcept {
std::cout << "Global delete " << std::endl;
std::free(ptr);
}

class MyClass {
public:
~MyClass() {
std::cout << "Деструктор MyClass вызван.\n";
}
};
int main() {
std::weak_ptr<MyClass> weak;

{
auto shared = std::make_shared<MyClass>();
weak = shared;

std::cout << "shared_ptr goes out of scope...\n";
} // shared уничтожается здесь

std::cout << "weak.expired(): " << weak.expired()
<< '\n'; // true
weak.reset();

std::cout << "All memory has been freed!\n";
}
// shared_ptr goes out of scope...
// Dtor MyClass called.
// weak.expired(): 1
// Global delete
// All memory has been freed!


Отметьте вызов деструктора после выхода из скоупа, но при этом память еще не освобождается. Она делает это только после reset и уничтожении последней слабой ссылки.

Учитывайте эти особенности, когда используете weak_ptr(например в кэше или списках слушателей).

Consider both sides of the coin. Stay cool.

#cpp11 #memory
1🔥17👍107❤‍🔥3😁1
​​Двойной unlock
#опытным

Если не пользоваться RAII, то можно наткнуться на массу проблем. Все знают про double free. Но менее известна проблема double unlock.

Все просто, вы используете ручной lock-unlock мьютекса и возможно попадаете в ситуацию двойного освобождения:

void unsafe_function(int value) {
mtx.lock();

if (value < 0) {
std::cout << "Error: negative value\n";
mtx.unlock();
// forget to return!
}

shared_data = value;
std::cout << "Data has updated: " << shared_data << std::endl;

mtx.unlock(); // second unlock
}


Практически всегда двойной unlock происходит из-за некорректного кода в той или иной степени. Забыть вызвать return кажется детской проблемой, но если вы например не написали тесты на эту ветку, то возможно вы наткнетесь на проблемы только в проде.

А проблемы могут быть примерно любыми. Потому что двойной unlock мьютекса - UB по стандарту. Соответственно, можете получить много непрятностей, от сегфолта до бесконечного ожидания.

Поэтому просто используйте RAII и спина болеть не будет:

void safe_function(int value) {
std::lock_guard lg{mtx};

if (value < 0) {
std::cout << "Error: negative value\n";
return;
}

shared_data = value;
std::cout << "Data has updated: " << shared_data << std::endl;
}


Use safe technics. Stay cool.

#concurrency #cpp11
👍2315🔥8😁3
​​enum class
#новичкам

Перечисления пришли в С++ еще из С и отлично живут. Однако плюсовикам не очень с ними комфортно работать с силу наследования слабой типизации и неявных преобразований enum'ов в числовые типы и в другие enum'ы

В С++11 появился новый тип перечислений - scoped enumerations. Или ограниченные областью видимости перечисления. Определяются они так:

enum class Enumeration {CATEGORY1, CATEGORY2, CATEGORY3};


Он решает две большие проблемы обычных перечислений:

👉🏿 Обычные перечисления неявно преобразуются в int и обратно, что вызывает ошибки, когда не предполагается использование перечисления в качестве целого числа.

Можно например попробовать получить следующее значение перечисления, просто прибавив единицу:

cpp
enum Color { RED, GREEN, BLUE };

Color c = RED;
Color next = c + 1; // Implicit conversion to int and visa versa!


Что значит прибавить красному цвету единицу - решительно непонятно.

Неявные преобразования enum class'ов же запрещено:

enum class Color { RED, GREEN, BLUE };

Color c = Color::RED;
Color next = c + 1; // ERROR!


Если вам сильно нужно преобразовать перечислитель к числу, то вы это должны сделать явно:

Color c = Color::RED;
Color next = static_cast<Color>(static_cast<int>(c) + 1);


👉🏿 Обычные перечисления экспортируют свои перечислители в окружающую область видимости, вызывая конфликты имён с другими сущностями в этой окружающей области:
enum Color { RED, GREEN, BLUE };

enum TrafficLight { RED, YELLOW, GREEN }; // ERROR!

void graphics_library() {
Color c = RED;
}


У scoped enum'ов такой проблемы нет. Имена перечислителей находятся в скоупе своего перечисления:

enum class Color1 { RED, GREEN, BLUE };
enum class Color2 { RED, GREEN, BLUE };

void graphics_library() {
Color1 c1 = Color1::RED;
Color2 c2 = Color2::RED;
}


И все прекрасно компилируется.

С учетом неймспейсов и любви к явным кастам в коммьюнити, в С++ лучше использовать enum class'ы вместо обычных перечислений.

Protect your scope. Stay cool.

#cppcore #cpp11
👍4116🔥15
​​enum struct
#опытным

В прошлом посте мы рассказали про enum class. И в 99.999% случаев эту сущность будут писать в коде именно, как enum class.

Но можно написать enum struct и это тоже будет работать!

enum class FileMode { Read, Write, Append };
enum struct LogLevel { Debug, Info, Warning, Error };

int main() {
FileMode mode = FileMode::Read;
LogLevel level = LogLevel::Info;
std::cout << (mode == FileMode::Read) << std::endl;
std::cout << (level == LogLevel::Info) << std::endl;
}


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

Вот такой короткий и бесполезный факт из мира плюсов)

Be useful. Stay cool.

#fun #cpp11
2👍37😁249🔥7🤣5
​​Размер enum'а
#опытным

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

Но тогда встает вопрос: а сколько весит enum? Мы же про эффективность и хотим, чтобы данные занимали минимально возможное пространство.

Мы можем явно написать:

enum MY_FAVOURITE_FRUITS
{
E_APPLE = 0x01,
E_WATERMELON = 0x02,
E_COCONUT = 0x04,
E_STRAWBERRY = 0x08,
E_CHERRY = 0x10,
E_PINEAPPLE = 0x20,
E_BANANA = 0x40,
E_MANGO = 0x80,
E_MY_FAVOURITE_FRUITS_FORCE8 = 0xFF // 'Force' 8bits, how can you tell?
};


Мы как бы явно говорим, что ограничиваем размер enum'а 8-ью битами. Но будет ли его размер реально 8 бит?

Не факт. Компилятор может выбрать любой подходящий по размеру тип, главное, чтобы он мог вместить все элементы перечисления. Это может быть char, short или int. И все это разного размера.

Неприятно, что на это нельзя было влиять.

Но прочь неприятности, потому что в С++11, помимо enum class появилась возможность указания размера scoped и unscoped enum'ов:

enum class E_MY_FAVOURITE_FRUITS : unsigned char
{
E_APPLE = 0x01,
E_WATERMELON = 0x02,
E_COCONUT = 0x04,
E_STRAWBERRY = 0x08,
E_CHERRY = 0x10,
E_PINEAPPLE = 0x20,
E_BANANA = 0x40,
E_MANGO = 0x80,
E_DEVIL_FRUIT = 0xFF
};


И теперь вы может контролировать и сами задавать размер перечисления.

Control your size. Stay cool.

#cpp11
25👍14🔥4😁4
​​Удобно превращаем enum в число
#опытным

В прошлом посте мы выяснили, что с С++11 можно самостоятельно указывать нижележащий тип, который и хранит все элементы enum'а.

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

Это важно, потому что scoped enum неявно не приводится к числам. Нам нужно явно указывать тип:

enum class ColorMask : std::uint32_t
{
red = 0xFF,
green = (red << 8),
blue = (green << 8),
alpha = (blue << 8)
};

// std::cout << ColorMask::red << std::endl; // ERROR
std::cout << static_cast<int>(ColorMask::red) << std::endl;


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

Современные IDE-шки возможно будут вам показывать нужный тип, а возможно и нет. Если тип enum'а явно указан, то можно взять его. Но если нет, то гадать не хочется. Хочется стандартного решения.

С++11 также вводит тип шаблонный тип std::underlying_type, который предоставляет зависимый тип type, содержащий подкапотный тип enum'a:

enum e1 {};
enum class e2 {};
enum class e3 : unsigned {};
enum class e4 : int {};

constexpr bool e1_t = std::is_same_v<std::underlying_type_t<e1>, int>;
constexpr bool e2_t = std::is_same_v<std::underlying_type_t<e2>, int>;
constexpr bool e3_t = std::is_same_v<std::underlying_type_t<e3>, int>;
constexpr bool e4_t = std::is_same_v<std::underlying_type_t<e4>, int>;

std::cout
<< "underlying type for 'e1' is " << (e1_t ? "int" : "non-int") << '\n'
<< "underlying type for 'e2' is " << (e2_t ? "int" : "non-int") << '\n'
<< "underlying type for 'e3' is " << (e3_t ? "int" : "non-int") << '\n'
<< "underlying type for 'e4' is " << (e4_t ? "int" : "non-int") << '\n';

// OUTPUT
// underlying type for 'e1' is non-int
// underlying type for 'e2' is int
// underlying type for 'e3' is non-int
// underlying type for 'e4' is int


Соответственно, для каста нужно сделать такую штуку:

auto num = static_cast<std::underlying_type_t<ColorMask>>(ColorMask::red);


Плохо, что это очень громоздкая конструкция, где к тому же типы повторяются. Поэтому в С++23 ввели хэлпер-сахарок std::to_underlying, который за нас все это делает:

auto num = std::to_underlying(ColorMask::red);


Красота!

Know your type. Stay cool.

#cpp11 #cpp23
👍21🔥149🥱1
Мувать не всегда дешево
#новичкам

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

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

Если вы придерживаетесь 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