Процесс и поток. Процесс
#новичкам
Сегодня поподробнее поговорим про процессы, зачем они нужны, какую роль играют и какой у них жизненный цикл. Но опять же в дебри лезть не будет, погрузимся только в важные моменты.
Процессы, как и все в этом мире, появилось в результате эволюции ОС и их подходов к управлению программами. Главный вызов - организация мультизадачности. Хочется, чтобы на одном компьютере могли исполняться множество программ.
Процесс - это абстракция ОС, предназначенная для удобного, безопасного и независимого исполнения кода различных программ на одном компьютере.
Пожалуй, самое важное здесь - процесс нужен для изоляции исполнения одной программы от исполнения другой. Изоляция достигается несколькими механизмами, обсудим самые важные:
⚡️ Механизм виртуальной памяти. У каждого процесса свое независимое ни от кого виртуальное пространство адресов. Все операции с памятью через ОС. Так она контролирует, чтобы один процесс не мог всякими неблагонадежными способами заполучить информацию из других процессов. Если он все-таки попытается что-то подобное сделать, то получит сигнал SIGSEGV.
У процессов конечно могут быть общие для доступа участки памяти(shared memory), но это делается явно и контролируемо.
⚡️Привилегии процессов. В современных ОС используется два уровня привилегии: режим ядра и пользовательский режим. Код пользовательского процесса не может выполнять привилегированные инструкции и не имеет прямого доступа к аппаратуре. Системные вызовы — единственный канал для взаимодействия с ядром, и они строго контролируются. Грубо говоря, пользовательский процесс(а это все процессы прикладных приложений) физически не может сделать ничего плохого на уровне ОС и повлиять на работу других программ.
Про изоляцию мы поняли, за счет нее обеспечивается безопасность железа и других программ. Но у процесса есть еще набор ресурсов, с помощью которых он взаимодействует с внешним миром:
1️⃣ PID - айдишник процесса. Это уникальный номер, который ОС присваивает каждому процессу в момент его создания. PID используется для идентификации процесса: по нему можно отправить сигнал, получить информацию о процессе или управлять им.
2️⃣ Когда программе нужно открыть файл, установить сетевое соединение или создать объект синхронизации, она обращается к ОС. В ответ ОС выдаёт дескриптор — небольшой идентификатор, который становится «пропуском» для работы с этим ресурсом. Практически любой ресурс, принадлежащий процессу, может быть представлен дескриптором: файлы, сокеты, каналы, устройства, таймеры и тд.
3️⃣ Переменные окружения. Это именованные строковые значения, которые ОС передаёт каждому процессу при его запуске. Они описывают среду, в которой работает программа: где искать исполняемые файлы, где хранить временные данные. Секреты для приложения также часто передаются через переменные окружения.
4️⃣ Память. Хоть процесс распоряжается виртуальной памятью, она все равно маппится на физические адреса. В памяти процесса содержатся стеки потоков, куча, код самой программы.
Хоть процессы и изолированы, но если в одном из них утекает память или дескрипторы, страдают от этого все.
Теперь чуть-чуть-чуть про цикл жизни
В Unix-подобных системах процесс создаётся системным вызовом
Процесс может завершиться самостоятельно, вызвав системный вызов
#OS #goodoldc
#новичкам
Сегодня поподробнее поговорим про процессы, зачем они нужны, какую роль играют и какой у них жизненный цикл. Но опять же в дебри лезть не будет, погрузимся только в важные моменты.
Процессы, как и все в этом мире, появилось в результате эволюции ОС и их подходов к управлению программами. Главный вызов - организация мультизадачности. Хочется, чтобы на одном компьютере могли исполняться множество программ.
Процесс - это абстракция ОС, предназначенная для удобного, безопасного и независимого исполнения кода различных программ на одном компьютере.
Пожалуй, самое важное здесь - процесс нужен для изоляции исполнения одной программы от исполнения другой. Изоляция достигается несколькими механизмами, обсудим самые важные:
⚡️ Механизм виртуальной памяти. У каждого процесса свое независимое ни от кого виртуальное пространство адресов. Все операции с памятью через ОС. Так она контролирует, чтобы один процесс не мог всякими неблагонадежными способами заполучить информацию из других процессов. Если он все-таки попытается что-то подобное сделать, то получит сигнал SIGSEGV.
У процессов конечно могут быть общие для доступа участки памяти(shared memory), но это делается явно и контролируемо.
⚡️Привилегии процессов. В современных ОС используется два уровня привилегии: режим ядра и пользовательский режим. Код пользовательского процесса не может выполнять привилегированные инструкции и не имеет прямого доступа к аппаратуре. Системные вызовы — единственный канал для взаимодействия с ядром, и они строго контролируются. Грубо говоря, пользовательский процесс(а это все процессы прикладных приложений) физически не может сделать ничего плохого на уровне ОС и повлиять на работу других программ.
Про изоляцию мы поняли, за счет нее обеспечивается безопасность железа и других программ. Но у процесса есть еще набор ресурсов, с помощью которых он взаимодействует с внешним миром:
1️⃣ PID - айдишник процесса. Это уникальный номер, который ОС присваивает каждому процессу в момент его создания. PID используется для идентификации процесса: по нему можно отправить сигнал, получить информацию о процессе или управлять им.
2️⃣ Когда программе нужно открыть файл, установить сетевое соединение или создать объект синхронизации, она обращается к ОС. В ответ ОС выдаёт дескриптор — небольшой идентификатор, который становится «пропуском» для работы с этим ресурсом. Практически любой ресурс, принадлежащий процессу, может быть представлен дескриптором: файлы, сокеты, каналы, устройства, таймеры и тд.
3️⃣ Переменные окружения. Это именованные строковые значения, которые ОС передаёт каждому процессу при его запуске. Они описывают среду, в которой работает программа: где искать исполняемые файлы, где хранить временные данные. Секреты для приложения также часто передаются через переменные окружения.
4️⃣ Память. Хоть процесс распоряжается виртуальной памятью, она все равно маппится на физические адреса. В памяти процесса содержатся стеки потоков, куча, код самой программы.
Хоть процессы и изолированы, но если в одном из них утекает память или дескрипторы, страдают от этого все.
Теперь чуть-чуть-чуть про цикл жизни
В Unix-подобных системах процесс создаётся системным вызовом
fork(). Он копирует текущий процесс в новый и ему присваивается свой уникальный PID. Оба процесса продолжают выполнение с инструкции, следующей сразу после вызова fork(). При этом в родительском потоке возвращается pid ребенка, а в ребенке возвращается 0:int main() {
pid_t pid = fork();
if (pid == 0) {
printf("Child: PID=%d, parent=%d\n", getpid(), getppid());
} else if (pid > 0) {
printf("Parent: PID=%d, child=%d\n", getpid(), pid);
} else {
perror("fork failed");
return 1;
}
return 0;
}Процесс может завершиться самостоятельно, вызвав системный вызов
exit(), или быть принудительно убитым сигналом (SIGKILL, SIGSEGV и др.). При завершении ядро освобождает все занятые ресурсы процесса и удаляет информацию о нем в своих структурах.#OS #goodoldc
❤25👍10🔥4
Процесс и поток. Поток
#новичкам
Чтобы ввести термин "поток" надо очень верхнеуровнего понять, как вообще код исполняется на ядре.
У нас есть 2 важных компоненты, без которых ничего не возможно. Это память и CPU. CPU скармливаются данные из памяти, он их пережевывает и выплевывает другие данные, которые также записываются в память.
CPU нужны операнды и инструкция, что с ними делать.
Начнем с инструкций
У нас есть скомпилированная программа и она в том числе содержит непосредственно код программы. Грубо говоря, это полотно инструкций, которые одну за одной надо исполнить на CPU.
Машинный код программы хранится в памяти процесса и для него нужен какой-то механизм исполнения.
Чтобы отслеживать, какая инструкция будет выполняться следующей, нужен специальный Program Counter(PC) или счетчик инструкций. Процесс такой: CPU загружает инструкцию, на которую указывает PC, увеличивает счетчик, декодирует инструкцию, делает нужные преобразования над операндами, сохраняет результат и цикл повторяется снова.
Теперь операнды
Для операций процессор чаще всего берёт операнды из регистров общего назначения, куда они предварительно загружаются из памяти.
Из какой?
Из 3-х областей памяти: стека, кучи и сегмента глобальных данных. Куча и глобальные данные шарятся между потоками исполнения и принадлежат процессу.
А вот как раз стек и связан с непосредственным обеспечением исполнения кода в моменте. Он нужен для работы функций. Каждый фрейм стека соответствует одной вызванной, но ещё не завершённой функции. Как только в коде вызывается функция, на стек кладется информация о ней. В основном это локальные переменные и адрес возврата. Так информация об исполнении всех вызванных в данный момент функциях остается в памяти.
Есть 2 важных регистра, которые связаны со стеком. Это base pointer(указатель на начало блока самой последней вызванной функции) и stack pointer - указатель на вершину стека.
По итогу информация из памяти(стека, кучи и тд) загружается в регистры общего назначения, откуда ею может пользоваться сам процессор.
Вот это очень грубо то, как исполняется код на процессорах.
Но этот процесс описывает исполнение одной программы. А в современных компьютерах есть многозадачность аля множество программ могут выполняться на одном ядре. Как этого достигают?
На самом деле стек, значения счетчика инструкций и регистров полностью определяют состояние исполнения текущего кода. Оно называется контекстом. Если где-то сохранить это состояние, то можно загрузить PС и регистры из другого контекста и продолжить исполнение уже другой программы, а исполнение изначальной прервется. Потом, когда наступит нужный момент, исполнение изначальной программы продолжится также с помощью загрузки ее сохраненного состояния.
Так вот по сути поток - это минимальная единица исполнения кода, которой ОС выделяет кванты времени для работы. Она владеет своим стеком и характеризуется конкретными значениями счетчика инструкций и регистрами, которые составляют контекст потока. Переключение между потоками выполняет планировщик ядра, сохраняя контекст одного потока и загружая другой.
Поток в программе может быть один и может быть несколько.
Все потоки шарят между собой общую кучу, сегмент глобальных данных, код программы и прочие ресурсы процесса типа дескрипторов.
И да, стеки потоков одного процесса тоже располагаются в памяти этого процесса. Просто потоки не могут легальными способами получить доступ к стекам других потоков того же процесса, чтобы не нарушать инкапсуляцию данных. Поток пользуется данными либо из своего стека, либо из общей кучи, либо из общих глобальных объектов.
Ну и совсем коротко о том, как явно в программе создать поток.
В С++ это делается через объект std::thread.
Ядро выделяет новый стек для потока, инициализирует его контекст так, чтобы выполнение началось с функции hello. После этого поток попадает в очередь планировщика и получает свой квант времени.
Share resources. Stay cool.
#OS #concurrency
#новичкам
Чтобы ввести термин "поток" надо очень верхнеуровнего понять, как вообще код исполняется на ядре.
У нас есть 2 важных компоненты, без которых ничего не возможно. Это память и CPU. CPU скармливаются данные из памяти, он их пережевывает и выплевывает другие данные, которые также записываются в память.
CPU нужны операнды и инструкция, что с ними делать.
Начнем с инструкций
У нас есть скомпилированная программа и она в том числе содержит непосредственно код программы. Грубо говоря, это полотно инструкций, которые одну за одной надо исполнить на CPU.
Машинный код программы хранится в памяти процесса и для него нужен какой-то механизм исполнения.
Чтобы отслеживать, какая инструкция будет выполняться следующей, нужен специальный Program Counter(PC) или счетчик инструкций. Процесс такой: CPU загружает инструкцию, на которую указывает PC, увеличивает счетчик, декодирует инструкцию, делает нужные преобразования над операндами, сохраняет результат и цикл повторяется снова.
Теперь операнды
Для операций процессор чаще всего берёт операнды из регистров общего назначения, куда они предварительно загружаются из памяти.
Из какой?
Из 3-х областей памяти: стека, кучи и сегмента глобальных данных. Куча и глобальные данные шарятся между потоками исполнения и принадлежат процессу.
А вот как раз стек и связан с непосредственным обеспечением исполнения кода в моменте. Он нужен для работы функций. Каждый фрейм стека соответствует одной вызванной, но ещё не завершённой функции. Как только в коде вызывается функция, на стек кладется информация о ней. В основном это локальные переменные и адрес возврата. Так информация об исполнении всех вызванных в данный момент функциях остается в памяти.
Есть 2 важных регистра, которые связаны со стеком. Это base pointer(указатель на начало блока самой последней вызванной функции) и stack pointer - указатель на вершину стека.
По итогу информация из памяти(стека, кучи и тд) загружается в регистры общего назначения, откуда ею может пользоваться сам процессор.
Вот это очень грубо то, как исполняется код на процессорах.
Но этот процесс описывает исполнение одной программы. А в современных компьютерах есть многозадачность аля множество программ могут выполняться на одном ядре. Как этого достигают?
На самом деле стек, значения счетчика инструкций и регистров полностью определяют состояние исполнения текущего кода. Оно называется контекстом. Если где-то сохранить это состояние, то можно загрузить PС и регистры из другого контекста и продолжить исполнение уже другой программы, а исполнение изначальной прервется. Потом, когда наступит нужный момент, исполнение изначальной программы продолжится также с помощью загрузки ее сохраненного состояния.
Так вот по сути поток - это минимальная единица исполнения кода, которой ОС выделяет кванты времени для работы. Она владеет своим стеком и характеризуется конкретными значениями счетчика инструкций и регистрами, которые составляют контекст потока. Переключение между потоками выполняет планировщик ядра, сохраняя контекст одного потока и загружая другой.
Поток в программе может быть один и может быть несколько.
Все потоки шарят между собой общую кучу, сегмент глобальных данных, код программы и прочие ресурсы процесса типа дескрипторов.
И да, стеки потоков одного процесса тоже располагаются в памяти этого процесса. Просто потоки не могут легальными способами получить доступ к стекам других потоков того же процесса, чтобы не нарушать инкапсуляцию данных. Поток пользуется данными либо из своего стека, либо из общей кучи, либо из общих глобальных объектов.
Ну и совсем коротко о том, как явно в программе создать поток.
В С++ это делается через объект std::thread.
void hello() {
std::cout << "Hello thread!\n";
}
int main() {
std::thread t(hello);
t.join();
}Ядро выделяет новый стек для потока, инициализирует его контекст так, чтобы выполнение началось с функции hello. После этого поток попадает в очередь планировщика и получает свой квант времени.
Share resources. Stay cool.
#OS #concurrency
🔥20❤11👍9😁4
Процесс и поток. Side‑by‑side
#новичкам
Мы разобрали, что процесс — это контейнер ресурсов, а поток — единица выполнения. Теперь давайте поставим их рядом и посмотрим, кто во что горазд. Будет еще немного новой информации, но подробности оставим за скобками.
👉🏿 Суть
Процесс - это окружения для выполнения программы. Поток - само исполнение.
👉🏿 Память
Каждый процесс имеет своё изолированное независимое виртуальное адресное пространство. В базе процесс не имеет доступа к данным другого процесса.
Все потоки одного процесса разделяют его память (кучу, глобальные переменные, код) и могут легко обращаться к одним и тем же данным.
👉🏿 Изоляция
Сбой одного процесса обычно не влияет на другие. ОС в базе запрещает одному процессу влезать в дела другого.
Сбой же в одном потоке (например, segmentation fault) убивает весь процесс. Потоки одного процесса легко обмениваются информацией
👉🏿 Создание и переключение
Процессы тяжело создавать(
Потоки же создавать легче: нужен только новый стек и небольшой блок управления(нужный например для сохранения контекста). Переключаются потоки сменой контекста aka подменой нескольких регистров.
👉🏿 Взаимодействие
Есть термин IPC - Inter Process Communication. Реализовывать межпроцессное взаимодействие можно с помощью пайпов, сокетов, shared memory, сигналов. Требует использования механизмов ядра.
С потоками чуть легче, они имеют доступ к одной и той же памяти. Только чтобы не нарваться на гонку данных, нужна синхронизация через мьютексы, атомики, барьеры.
👉🏿 Ресурсы
Процесс владеет всей своей памятью(кодом, кучей, сегментом глобальных данных), а также файловыми дескрипторами и переменными окружения.
Потоки же имеют доступ ко всем этим ресурсам + у них есть персональные стеки и набор регистров.
👉🏿 ID
У процесса есть PID, с помощью которого ОС идентифицирует процесс.
У каждого потока также есть свои айдишники - TID, и они принадлежат одному PID.
Как-то так. Надеюсь, что теперь у вас есть понимание, что вопрос о разнице между потоком и процессом почти эквивалентен вопросу о разнице головы и зубов. Это хоть и тесно связанные, но абсолютно разные понятия.
See the difference. Stay cool.
#OS
#новичкам
Мы разобрали, что процесс — это контейнер ресурсов, а поток — единица выполнения. Теперь давайте поставим их рядом и посмотрим, кто во что горазд. Будет еще немного новой информации, но подробности оставим за скобками.
👉🏿 Суть
Процесс - это окружения для выполнения программы. Поток - само исполнение.
👉🏿 Память
Каждый процесс имеет своё изолированное независимое виртуальное адресное пространство. В базе процесс не имеет доступа к данным другого процесса.
Все потоки одного процесса разделяют его память (кучу, глобальные переменные, код) и могут легко обращаться к одним и тем же данным.
👉🏿 Изоляция
Сбой одного процесса обычно не влияет на другие. ОС в базе запрещает одному процессу влезать в дела другого.
Сбой же в одном потоке (например, segmentation fault) убивает весь процесс. Потоки одного процесса легко обмениваются информацией
👉🏿 Создание и переключение
Процессы тяжело создавать(
fork() копирует таблицы страниц, дескрипторы, структуры ядра) и переключать(требуется смена таблиц страниц)Потоки же создавать легче: нужен только новый стек и небольшой блок управления(нужный например для сохранения контекста). Переключаются потоки сменой контекста aka подменой нескольких регистров.
👉🏿 Взаимодействие
Есть термин IPC - Inter Process Communication. Реализовывать межпроцессное взаимодействие можно с помощью пайпов, сокетов, shared memory, сигналов. Требует использования механизмов ядра.
С потоками чуть легче, они имеют доступ к одной и той же памяти. Только чтобы не нарваться на гонку данных, нужна синхронизация через мьютексы, атомики, барьеры.
👉🏿 Ресурсы
Процесс владеет всей своей памятью(кодом, кучей, сегментом глобальных данных), а также файловыми дескрипторами и переменными окружения.
Потоки же имеют доступ ко всем этим ресурсам + у них есть персональные стеки и набор регистров.
👉🏿 ID
У процесса есть PID, с помощью которого ОС идентифицирует процесс.
У каждого потока также есть свои айдишники - TID, и они принадлежат одному PID.
Как-то так. Надеюсь, что теперь у вас есть понимание, что вопрос о разнице между потоком и процессом почти эквивалентен вопросу о разнице головы и зубов. Это хоть и тесно связанные, но абсолютно разные понятия.
See the difference. Stay cool.
#OS
👍25❤11🔥9
Конструктор и инвариант
#новичкам
У нас есть 2 варианта, как мы можем наполнять объект данными.
1️⃣ Делаем все поля публичными, не определяем ни одного конструктор и используем агрегатную инициализацию
2️⃣ Делаем конструктор и заполняем приватные поля в нем
Как понять, какой вариант выбрать, когда в следующий раз придется писать новый класс? Будем разбираться
Для того, чтобы агрегатная инциализация была корректным способом создания объекта, должны быть выполнены некоторые условия:
👉🏿 Поля должны быть независимы друг от друга.
👉🏿 Поля должны иметь возможность представлять собой весь спектр значений своего типа.
👉🏿 Нет дополнительной логики, которая срабатывает на каждое создание объекта.
Эти 3 пункта гарантируют, что наш класс - это просто набор каких-то значений. Мы можем собрать этот набор частично, поменять содержимое по середине пути и ничего плохого не произойдет.
В противном же случае в вашем классе есть инвариант, который важно сохранять для каждого объекта.
Например, в объекте типа даты, который создается из строки, нужно проверять, валидная ли дата передана. В объекте типа математического интервала левый конец должен быть меньше либо равен правому. Или при создании какого-то объекта мы накручиваем счетчик из метрики.
И вот как раз для того, чтобы вся логика проверки и обеспечения инварианта объекта была внутри кода класса, а не в клиентском коде, нужно определять конструктор. Так детали реализации будут инкапсулированы внутрь класса. Это позволит абстрагироваться от них в клиентском коде и даже если логика инварианта изменится, то это никак не затронет внешний код.
Странно было бы на пользователя возлагать ответственность за проверку соответствия размера и содержимого буфера. Они ведь такие безответственные.
Ну и в будущем, если мы захотим запретить вообще передавать в буффер nullptr, то это изменение коснется только кода класса.
Don't give details to a client. Stay cool.
#design #goodpractice
#новичкам
У нас есть 2 варианта, как мы можем наполнять объект данными.
1️⃣ Делаем все поля публичными, не определяем ни одного конструктор и используем агрегатную инициализацию
struct Order {
std::string client_id;
std::string item_id;
float amount;
};
Example ex = {"123", "1234", 42.0};2️⃣ Делаем конструктор и заполняем приватные поля в нем
class Clock {
int hour, minute;
public:
Clock(int h, int m) : hour(h), minute(m) {
...
}
};
auto cl = Clock(12, 34)Как понять, какой вариант выбрать, когда в следующий раз придется писать новый класс? Будем разбираться
Для того, чтобы агрегатная инциализация была корректным способом создания объекта, должны быть выполнены некоторые условия:
👉🏿 Поля должны быть независимы друг от друга.
👉🏿 Поля должны иметь возможность представлять собой весь спектр значений своего типа.
👉🏿 Нет дополнительной логики, которая срабатывает на каждое создание объекта.
Эти 3 пункта гарантируют, что наш класс - это просто набор каких-то значений. Мы можем собрать этот набор частично, поменять содержимое по середине пути и ничего плохого не произойдет.
В противном же случае в вашем классе есть инвариант, который важно сохранять для каждого объекта.
Например, в объекте типа даты, который создается из строки, нужно проверять, валидная ли дата передана. В объекте типа математического интервала левый конец должен быть меньше либо равен правому. Или при создании какого-то объекта мы накручиваем счетчик из метрики.
И вот как раз для того, чтобы вся логика проверки и обеспечения инварианта объекта была внутри кода класса, а не в клиентском коде, нужно определять конструктор. Так детали реализации будут инкапсулированы внутрь класса. Это позволит абстрагироваться от них в клиентском коде и даже если логика инварианта изменится, то это никак не затронет внешний код.
class BufferView {
const char* data;
size_t size;
public:
BufferView(const char* ptr, size_t len) : data(ptr), size(len) {
if (size > 0 && data == nullptr)
throw std::runtime_error("BufferView error: nonzero size while data is nullptr");
}
};Странно было бы на пользователя возлагать ответственность за проверку соответствия размера и содержимого буфера. Они ведь такие безответственные.
Ну и в будущем, если мы захотим запретить вообще передавать в буффер nullptr, то это изменение коснется только кода класса.
Don't give details to a client. Stay cool.
#design #goodpractice
👍15❤7🔥7😁2🎄2
Дефолтный конструктор. Введение
#новичкам
Описание корректного интерфейса создания объекта - важная часть проектирования класса. Поэтому надо понимать нюансики работы с конструкторами, чтобы все правильно организовать.
Ну и конечно самый базовый и, потенциально, самый сложный с точки зрения языка - конструктор по-умолчанию.
Казалось бы, у Example вообще не определено ни одного конструктора. Но тем не менее объект успешно создался. Как так?
Дефолтный конструктор умеет за вас генерировать сам компилятор.
А почему бы и нет? Смотря со стороны даже вполне логично и понятно, что он должен делать: вызывать конструкторы по умолчанию для всех нестатических членов и базовых классов в порядке объявления. Если мне от дефолтного конструктора нужно только это, то я могу просто положиться на компилятор.
Таким образом конструктор по-умолчанию входит в число специальных методов классов, которые компилятор сам умеет генерить.
Однако, не все так просто.
Как только вы определите хотя бы один другой конструктор, компилятор перестанет генерить дефолтный:
Оно и понятно: если вы не определяли никакой конструктор, значит вы довольны дефолтным поведением. Но как только вы сами определили конструктор, вы сказали компилятору, что дефолтное поведение вам не подходит и вы берете ответственность за способы создания объектов этого класса.
И компилятор не смеет перечить вашей задумке. Очень может быть, что вы хотите, чтобы Example2 создавался только через параметрический конструктор и больше никак. По сути, именно это и прописано сейчас в классе. Если бы компилятор неявно добавил конструктор по-умолчанию, то это нарушило бы контракт вашего класса.
Если вы все-таки хотите, чтобы у вас была возможность создать объект по-умолчанию, то вам явно нужно добавить конструктор без аргументов:
О том, какую роль 50 оттенков конструктора по-умолчанию при обращении с объектами классов, мы поговорим в следующем посте.
Don't be trivial. Stay cool.
#cppcore
#новичкам
Описание корректного интерфейса создания объекта - важная часть проектирования класса. Поэтому надо понимать нюансики работы с конструкторами, чтобы все правильно организовать.
Ну и конечно самый базовый и, потенциально, самый сложный с точки зрения языка - конструктор по-умолчанию.
class NoConstructor {
int total;
public:
void accumulate (int x) { total += x; }
};
Example ex;Казалось бы, у Example вообще не определено ни одного конструктора. Но тем не менее объект успешно создался. Как так?
Дефолтный конструктор умеет за вас генерировать сам компилятор.
А почему бы и нет? Смотря со стороны даже вполне логично и понятно, что он должен делать: вызывать конструкторы по умолчанию для всех нестатических членов и базовых классов в порядке объявления. Если мне от дефолтного конструктора нужно только это, то я могу просто положиться на компилятор.
Таким образом конструктор по-умолчанию входит в число специальных методов классов, которые компилятор сам умеет генерить.
Однако, не все так просто.
Как только вы определите хотя бы один другой конструктор, компилятор перестанет генерить дефолтный:
class ParametrizedConstructor {
int total;
public:
ParametrizedConstructor(int initial_value) : total(initial_value) { };
void accumulate (int x) { total += x; };
};
ParametrizedConstructor ex (100); // ok
ParametrizedConstructor ex1; // error: no default constructorОно и понятно: если вы не определяли никакой конструктор, значит вы довольны дефолтным поведением. Но как только вы сами определили конструктор, вы сказали компилятору, что дефолтное поведение вам не подходит и вы берете ответственность за способы создания объектов этого класса.
И компилятор не смеет перечить вашей задумке. Очень может быть, что вы хотите, чтобы Example2 создавался только через параметрический конструктор и больше никак. По сути, именно это и прописано сейчас в классе. Если бы компилятор неявно добавил конструктор по-умолчанию, то это нарушило бы контракт вашего класса.
Если вы все-таки хотите, чтобы у вас была возможность создать объект по-умолчанию, то вам явно нужно добавить конструктор без аргументов:
class ParametrizedAndDefaultConstructor {
int total;
public:
ParametrizedAndDefaultConstructor() = default; // или так
ParametrizedAndDefaultConstructor() {} // или так
ParametrizedAndDefaultConstructor(int initial_value) : total(initial_value) { };
void accumulate (int x) { total += x; };
};
ParametrizedAndDefaultConstructor ex(100); // ok
ParametrizedAndDefaultConstructor ex1; // okParametrizedAndDefaultConstructor() = default; - вы явно определяете конструктор по-умолчанию, но так же явно просите компилятор о том, чтобы его поведение было как если бы сам компилятор его генерировал.ParametrizedAndDefaultConstructor() {/something/} - это вы уже самостоятельно определяете, какую дополнительную логику должен иметь этот конструктор. Он делает все то же самое, что и тривиальный, только вдобавок выполняется еще и то, что вы указали внутри фигурных скобок. Такой конструктор называется уже нетривиальным, даже если его тело в итоге оказалось пустым.О том, какую роль 50 оттенков конструктора по-умолчанию при обращении с объектами классов, мы поговорим в следующем посте.
Don't be trivial. Stay cool.
#cppcore
❤18👍12🔥6
Дефолтные конструкторы. Такие разные
#опытным
Существует 3 вида дефолтных конструкторов и важно понимать, в чем у них различия, чтобы разрешать нужную функциональность.
1️⃣ Тривиальный дефолтный конструктор
По сути задача дефолтного конструктора - вызывать у своих полей и подклассов дефолтный конструктор. А что если все поля класса имеют тривиальные типы?
На самом деле у всех тривиальных базовых типов в С++ есть конструктор по-умолчанию. Но он тривиальный. То есть ничего вообще не делает и никак не инициализирует объект.
И это свойство может передастся конструктору Foo. Если для такого типа дефолтный конструктор будет сгенерирован компилятором, то он тоже ничего делать не будет.
Конструктор по-умолчанию называется тривиальным, если:
👉🏿 У класса нет виртуальных методов.
👉🏿 Все базы класса имеют тривиальные конструкторы по-умолчанию.
👉🏿 Все поля класса имеют тривиальные конструкторы по-умолчанию.
Все типы, которые совместимы с С, обладают этим свойством. Если вам нужна такая совместимость, тривиальный конструктор по-умолчанию - это ваш бро.
2️⃣ Нетривиальный дефолтный конструктор, сгенерированный компилятором
Если ваш класс содержит поле, у которого есть нетривиальный конструктор, то вам скорее всего и не нужна С-совместимость. Но вы можете приобрести кое-что другое.
Предоставив компилятору честь сгенерировать конструктор, вы разрешаете инициализировать объект с помощью агрегатной инициализации:
можете даже designated initialization воспользоваться:
Там есть конечно еще несколько требований, но опустим их, чтобы не сбивать фокус.
3️⃣ Нетривиальный пользовательский конструктор по-умолчанию
Как только вы сами определили дефолтный конструктор, то вы сразу же попали в эту категорию. И лишились преимуществ, описанных выше. Даже если вы определили пустой конструктор, он все равно считается кастомным:
Да, с инициализацией у С++ довольно сложные отношения, но много чего идет от наследия С и обратной совместимости.
Don't be trivial. Stay cool.
#cppcore
#опытным
Существует 3 вида дефолтных конструкторов и важно понимать, в чем у них различия, чтобы разрешать нужную функциональность.
1️⃣ Тривиальный дефолтный конструктор
По сути задача дефолтного конструктора - вызывать у своих полей и подклассов дефолтный конструктор. А что если все поля класса имеют тривиальные типы?
struct Foo {
int i;
double d;
char c;
};На самом деле у всех тривиальных базовых типов в С++ есть конструктор по-умолчанию. Но он тривиальный. То есть ничего вообще не делает и никак не инициализирует объект.
И это свойство может передастся конструктору Foo. Если для такого типа дефолтный конструктор будет сгенерирован компилятором, то он тоже ничего делать не будет.
Конструктор по-умолчанию называется тривиальным, если:
👉🏿 У класса нет виртуальных методов.
👉🏿 Все базы класса имеют тривиальные конструкторы по-умолчанию.
👉🏿 Все поля класса имеют тривиальные конструкторы по-умолчанию.
Все типы, которые совместимы с С, обладают этим свойством. Если вам нужна такая совместимость, тривиальный конструктор по-умолчанию - это ваш бро.
2️⃣ Нетривиальный дефолтный конструктор, сгенерированный компилятором
Если ваш класс содержит поле, у которого есть нетривиальный конструктор, то вам скорее всего и не нужна С-совместимость. Но вы можете приобрести кое-что другое.
Предоставив компилятору честь сгенерировать конструктор, вы разрешаете инициализировать объект с помощью агрегатной инициализации:
struct Foo {
// Foo() = default; так тоже можно
int i;
std::string s;
std::vector<int> v;
};
Foo f = {42, "Hello World", {1, 2, 3}};можете даже designated initialization воспользоваться:
Foo f = {.i = 42, .s = "Hello World", .v = {1, 2, 3}};Там есть конечно еще несколько требований, но опустим их, чтобы не сбивать фокус.
3️⃣ Нетривиальный пользовательский конструктор по-умолчанию
Как только вы сами определили дефолтный конструктор, то вы сразу же попали в эту категорию. И лишились преимуществ, описанных выше. Даже если вы определили пустой конструктор, он все равно считается кастомным:
struct Foo {
Foo() {};
int i;
std::string s;
std::vector<int> v;
};
Foo f = {42, "Hello World", {1, 2, 3}}; // ERRORДа, с инициализацией у С++ довольно сложные отношения, но много чего идет от наследия С и обратной совместимости.
Don't be trivial. Stay cool.
#cppcore
👍17❤13🔥6
Дефолтный конструктор. Ограничения
#новичкам
Мы уже краем уха задевали эту тему, но сегодня поговорим основательно. Вот все причины, почему дефолтный конструктор не может неявно сгенерироваться компилятором:
1️⃣ У класса есть другие конструкторы
Даже если вы сами определили copy/move конструктор руками, конструктор по-умолчанию неявно генерироваться не будет:
2️⃣ В классе есть нестатическое поле-ссылка.
Ссылка обязательно должна быть инициализирована объектом, а это невозможно сделать, не имея объект.
3️⃣ В классе есть нестатическое константное поле с тривиальным конструктором по-умолчанию. Тривиальные конструкторы по сути вообще ничего не делают, кроме как начинают лайфтайм объекта. То есть все поля заполняются мусором.
Но для константных объектов подразумевается наличие определенного постоянного значения. Мусорные значения не удовлетворяют этому требованию.
Тем не менее, компилятор спокойно может проглотить константный член с нетривиальным дефолтным конструктором. Считается, что такой конструктор корректно инициализирует объект:
4️⃣ Если какой-то член класса или базовый класс имеет недоступный (private) или удалённый (
Если знаете еще способы, пишите в комментах.
Don't be trivial. Stay cool.
#cppcore
#новичкам
Мы уже краем уха задевали эту тему, но сегодня поговорим основательно. Вот все причины, почему дефолтный конструктор не может неявно сгенерироваться компилятором:
1️⃣ У класса есть другие конструкторы
class ParametrizedConstructor {
int total;
public:
ParametrizedConstructor(int initial_value) : total(initial_value) { };
void accumulate (int x) { total += x; };
};
Example2 ex (100); // ok
Example2 ex1; // error: no default constructorДаже если вы сами определили copy/move конструктор руками, конструктор по-умолчанию неявно генерироваться не будет:
struct UserCopyConstructor
{
UserCopyConstructor(const UserCopyConstructor&) {}
// UserCopyConstructor::UserCopyConstructor() is implicitly defined as deleted
};
struct UserMoveConstructor
{
UserMoveConstructor(UserMoveConstructor&&) = default;
// UserMoveConstructor::UserMoveConstructor() is implicitly defined as deleted
};
2️⃣ В классе есть нестатическое поле-ссылка.
Ссылка обязательно должна быть инициализирована объектом, а это невозможно сделать, не имея объект.
struct HasReference {
int& ref; // error: reference needs to be initialized
};3️⃣ В классе есть нестатическое константное поле с тривиальным конструктором по-умолчанию. Тривиальные конструкторы по сути вообще ничего не делают, кроме как начинают лайфтайм объекта. То есть все поля заполняются мусором.
Но для константных объектов подразумевается наличие определенного постоянного значения. Мусорные значения не удовлетворяют этому требованию.
struct ConstTrivial {
const int x; // int has a trivial default constructor, so cannot default construct an object
};Тем не менее, компилятор спокойно может проглотить константный член с нетривиальным дефолтным конструктором. Считается, что такой конструктор корректно инициализирует объект:
struct ConstNonTrivial {
const std::string s; // std::string has non-trivial constructor
};
ConstNonTrivial cnt; // OK, s is just empty string4️⃣ Если какой-то член класса или базовый класс имеет недоступный (private) или удалённый (
= delete) конструктор по умолчаниюstruct NoDefault {
private:
NoDefault() {} // private constructor
};
struct Derived : NoDefault {
// Derived() is implicitly deleted
};
struct DeletedDefault {
DeletedDefault() = delete; // explicilty deleted
};
struct HasDeleted {
DeletedDefault dd;
};
HasDeleted hd; // ERROR! dd has explicilty deleted default constructorЕсли знаете еще способы, пишите в комментах.
Don't be trivial. Stay cool.
#cppcore
❤13🔥7👍3😁2
Потоки и линукс
#опытным
Описанное вот тут касательно процессов и поток в общем отражает концепцию того, что происходит в ОС, но детали реализации всего этого добра в разных ОС - разные.
И сегодня хочется поговорить об особенностях конкретно линукса.
В Linux нет потоков.
Нет системного вызова
Никогда не было.
Когда вы вызываете pthread_create или конструктор std::thread, Linux не создаёт «поток». Он создаёт задачу.
Такую же задачу, как когда вызывается сискол fork. Отличие только в разделяемых ресурсах.
И ядро оперирует только этими задачами. Оно не разделяет потоки и процессы. Эти понятия описывают разные полюса одного понятия "задача".
Есть сискол, который создает дочернюю задачу - clone. Так вот параметры этого системного вызова и определяют ее дальнейшую судьбу.
Вызов clone без параметров поведенчески эквивалентен вызову fork: вы получаете новый полностью независимый процесс со своим виртуальным пространством, дескрипторами и тд. Но передайте в параметры набор флагов
Повторюсь, единственное отличие - различный набор разделяемых ресурсов.
Кто хочет чуть подробнее про все это почитать - вот хорошая статейка. Правда вам будут нужны заветные 3 буквы.
Know the truth. Stay cool.
#OS #concurrency
#опытным
Описанное вот тут касательно процессов и поток в общем отражает концепцию того, что происходит в ОС, но детали реализации всего этого добра в разных ОС - разные.
И сегодня хочется поговорить об особенностях конкретно линукса.
В Linux нет потоков.
Нет системного вызова
thread_create().Никогда не было.
Когда вы вызываете pthread_create или конструктор std::thread, Linux не создаёт «поток». Он создаёт задачу.
Такую же задачу, как когда вызывается сискол fork. Отличие только в разделяемых ресурсах.
И ядро оперирует только этими задачами. Оно не разделяет потоки и процессы. Эти понятия описывают разные полюса одного понятия "задача".
Есть сискол, который создает дочернюю задачу - clone. Так вот параметры этого системного вызова и определяют ее дальнейшую судьбу.
Вызов clone без параметров поведенчески эквивалентен вызову fork: вы получаете новый полностью независимый процесс со своим виртуальным пространством, дескрипторами и тд. Но передайте в параметры набор флагов
CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND | CLONE_THREAD и получите процесс, который по своей сути POSIX поток, который шарит эти ресурсы со своей родительской задачей. Такие процессы называют легковесные процессы или LWP.Повторюсь, единственное отличие - различный набор разделяемых ресурсов.
Кто хочет чуть подробнее про все это почитать - вот хорошая статейка. Правда вам будут нужны заветные 3 буквы.
Know the truth. Stay cool.
#OS #concurrency
👍20❤8🔥4
Специальные методы классов
#новичкам
Помимо обычных методов, которые позволяют классу выполнить какую-то полезную работу, существуют методы, которые менеджерят самими объектами. Среди них выделяют так называемые "специальные" методы классов, которые компилятор неявно за вас сгенерирует. Давайте перечислим их и поясним, почему компилятор способен вообще для них написать код:
1️⃣ Конструктор по-умолчанию. Дефолтный конструктор без аргументов. Мы уже подробно разбирали его в предыдущих постах, останавливаться не будем.
Любые параметрические конструкторы не могут быть специальными методами классов, аля не могут быть сгенерированы компилятором, потому что он не знает, что в общем случае нужно делать с параметрами.
2️⃣ Деструктор. Он семантически разрушает объект. Забирает у него память, закрывает дескрипторы и соединения, высасывает жизнь, отбирает квартиру, машину, жену и собаку и передает это государству операционной системе.
Как так получается, что компилятор вообще способен сам сгенерировать код деструктора объекта, который владеет ресурсами?
Все благодаря RAII и способности деструктора вызывать деструкторы своих полей. Если в вашем классе Foo содержится std::vector или std::unique_ptr, вам не нужно задумываться о менеджменте памяти, авторы этих классов уже все придумали за вас. Вам нужно лишь вызывать их деструктор, что деструктор Foo делает автоматически.
3️⃣ Копирующий конструктор
4️⃣ Перемещающий конструктор
Для этой пары конструкторов правило единое: самое простое решение для копирования/перемещения объекта - это просто скопировать/переместить все его поля в другой объект. Компилятор с этим вполне справится сам: вызовет пачку нужных конструкторов да и все.
Естественно, если хотя бы у одного поля вашего класса не будет копирующего/перемещающего конструктора, сам ваш класс потеряет возможность копироваться/перемещаться.
5️⃣ Копирующий оператор присваивания
6️⃣ Перемещающий оператор присваивания
Это операторы, которые вызываются тогда, когда вы хотите передать уже существующему объекту значение другого объекта.
И вот здесь интересная комбинация.
Чтобы присвоить значение, нужно уметь разрушать текущее значение и передавать другое значение. То есть автоматическая генерация присваивания возможно благодаря тому, что компилятор сам умеет разрушать объект и копировать/присваивать значения полей.
Чуть остановимся на синтаксисе:
Последняя операция не является присваиванием - это вызов конструктора(в этом случае копирующего). Если объект только создается, то это всегда вызов конструктора.
Теперь примерчик:
Итого: всего 6 специальных методов класса. Не больше, не меньше. На собесах любят про это спрашивать, так что забирайте.
Be special. Stay cool.
#cppcore #cpp11 #interview
#новичкам
Помимо обычных методов, которые позволяют классу выполнить какую-то полезную работу, существуют методы, которые менеджерят самими объектами. Среди них выделяют так называемые "специальные" методы классов, которые компилятор неявно за вас сгенерирует. Давайте перечислим их и поясним, почему компилятор способен вообще для них написать код:
1️⃣ Конструктор по-умолчанию. Дефолтный конструктор без аргументов. Мы уже подробно разбирали его в предыдущих постах, останавливаться не будем.
Любые параметрические конструкторы не могут быть специальными методами классов, аля не могут быть сгенерированы компилятором, потому что он не знает, что в общем случае нужно делать с параметрами.
2️⃣ Деструктор. Он семантически разрушает объект. Забирает у него память, закрывает дескрипторы и соединения
Как так получается, что компилятор вообще способен сам сгенерировать код деструктора объекта, который владеет ресурсами?
Все благодаря RAII и способности деструктора вызывать деструкторы своих полей. Если в вашем классе Foo содержится std::vector или std::unique_ptr, вам не нужно задумываться о менеджменте памяти, авторы этих классов уже все придумали за вас. Вам нужно лишь вызывать их деструктор, что деструктор Foo делает автоматически.
using File = std::unique_ptr<FILE, decltype(&std::fclose)>;
struct Foo {
std::vector<int> numbers;
File file;
// no explicit destructor ~Foo() {}
};
void foo() {
Foo obj = {{1, 2, 3, 4, 5, 6, 7, 8, 9}, {std::fopen("example.txt", "r"), &std::fclose}};
} // destroy obj here
3️⃣ Копирующий конструктор
4️⃣ Перемещающий конструктор
Для этой пары конструкторов правило единое: самое простое решение для копирования/перемещения объекта - это просто скопировать/переместить все его поля в другой объект. Компилятор с этим вполне справится сам: вызовет пачку нужных конструкторов да и все.
Естественно, если хотя бы у одного поля вашего класса не будет копирующего/перемещающего конструктора, сам ваш класс потеряет возможность копироваться/перемещаться.
struct Foo {
std::vector<int> numbers;
// no explicit copy/move constructors
// Foo(const Foo& other) {...}
// Foo(Foo&& other) {...}
};
void foo() {
Foo obj = {{1, 2, 3, 4, 5, 6, 7, 8, 9}};
Foo copied_in_obj = obj; // copy obj
Foo moved_in_obj = std::move(obj); // move from obj5️⃣ Копирующий оператор присваивания
6️⃣ Перемещающий оператор присваивания
Это операторы, которые вызываются тогда, когда вы хотите передать уже существующему объекту значение другого объекта.
И вот здесь интересная комбинация.
Чтобы присвоить значение, нужно уметь разрушать текущее значение и передавать другое значение. То есть автоматическая генерация присваивания возможно благодаря тому, что компилятор сам умеет разрушать объект и копировать/присваивать значения полей.
Чуть остановимся на синтаксисе:
Foo a, b;
a = b; // copy assign
a = std::move(b); // move assign
Foo a1 = b; // THIS IS NOT AN ASSIGN
Последняя операция не является присваиванием - это вызов конструктора(в этом случае копирующего). Если объект только создается, то это всегда вызов конструктора.
Теперь примерчик:
struct Foo {
std::vector<int> numbers;
// no explicit copy/move constructors
// Foo& operator=(const Foo& other) {...}
// Foo& operator=(Foo&& other) {...}
};
void foo() {
Foo forward = {{1, 2, 3, 4, 5, 6, 7, 8, 9}};
Foo backward = {{9, 8, 7, 6, 5, 4, 3, 2, 1}};
Foo obj; // created obj here
obj = forward; // copy assign forward to obj
obj = std::move(backward); // move assign backward to objИтого: всего 6 специальных методов класса. Не больше, не меньше. На собесах любят про это спрашивать, так что забирайте.
Be special. Stay cool.
#cppcore #cpp11 #interview
🔥10👍7❤6
Замечал странную штуку: дел не так уж много, но любое – как будто через сопротивление?
Не то чтобы лень. Просто не делается и все тут! Зато видосики на Ютубе залетают на ура...
Попался годный разбор, советую посмотреть, если тоже чувствуешь, что превращаешься в апатичного зомби 👉🏼 https://t.me/Manifestans
Мысль, которая зашла: когда перестаешь понимать "чего хочу Я", даже нормальная жизнь ощущается, как каторга.
Кликай сюда, чтобы разобраться, что с тобой происходит и как снова начать испытывать ощущение, что ты живешь, а не существуешь.
Не то чтобы лень. Просто не делается и все тут! Зато видосики на Ютубе залетают на ура...
Попался годный разбор, советую посмотреть, если тоже чувствуешь, что превращаешься в апатичного зомби 👉🏼 https://t.me/Manifestans
Мысль, которая зашла: когда перестаешь понимать "чего хочу Я", даже нормальная жизнь ощущается, как каторга.
Кликай сюда, чтобы разобраться, что с тобой происходит и как снова начать испытывать ощущение, что ты живешь, а не существуешь.
👍6👎4🔥4❤2😁2😭2
Правило 5
#новичкам
Так много разных способов определять специальные методы класса, что голова кругом. Помечать default, самому определять, доверяться компилятору, удалять, использовать обертки и тд.
Сложно, в общем.
Ну если есть что-то сложное, то люди всегда пытаются как-то его упростить и свести к набору понятных правил.
В С++ тоже есть парочка таких правил. И начнем с самого узнаваемого.
Правило 5
Оно говорит о том, что если вы сами определили или удалили хотя бы один из 5 специальных методов класса, то остальные вам тоже нужно определить(удалить) самим.
Так стоп. Почему 5, если специальных методов класса 6?
Ответ напрямую проистекает из идеи, зачем это правило вообще нужно.
Правило придумали для тех классов, которые используют нетривиальную логику управления своими ресурами.
Допустим вы зачем-то хотите хранить строку в объекте. И не как нормальные люди, а вот нетривиально, как указатель:
Там где new, обязательно должен быть delete:
Так вот правило нам говорит, что раз мы определили деструктор, мы как-то необычно менеджим ресурсы в нашем классе. А если это так, то нам вряд ли подойдет, что сгенерирует компилятор. Более того, компилятор откажется сам неявно генерировать определенные методы. Поэтому нужно вручную определить все 5 специальных методов.
Ну и быстро допишем все:
Мы определили деструктор, копи и мув конструкторы, а также операторы копи и мув присваивания.
Где же конструктор по-умолчанию?
1️⃣ Он не управляет ресурсами, он лишь даем им значение по умолчанию.
2️⃣ Не всегда он даже нужен, а иногда даже вреден
Что будет, если я создам кольцевой буфер по-умолчанию и попытаюсь в него запихать элемент? Будет не то же самое, что и с std::vector, не выделится новая память. В худшем случае будет запись в невыделенную память. В лучшем - будет написана проверка на дурака.
Но зачем эта проверка, когда можно просто не писать конструктор по-умолчанию и избежать ошибок?
То есть для полноценного управления ресурсами достаточно копи/мув операторов, копи/мув конструкторов и деструктора.
Follow the rules. Stay cool.
#design #goodpractice
#новичкам
Так много разных способов определять специальные методы класса, что голова кругом. Помечать default, самому определять, доверяться компилятору, удалять, использовать обертки и тд.
Сложно, в общем.
Ну если есть что-то сложное, то люди всегда пытаются как-то его упростить и свести к набору понятных правил.
В С++ тоже есть парочка таких правил. И начнем с самого узнаваемого.
Правило 5
Оно говорит о том, что если вы сами определили или удалили хотя бы один из 5 специальных методов класса, то остальные вам тоже нужно определить(удалить) самим.
Так стоп. Почему 5, если специальных методов класса 6?
Ответ напрямую проистекает из идеи, зачем это правило вообще нужно.
Правило придумали для тех классов, которые используют нетривиальную логику управления своими ресурами.
Допустим вы зачем-то хотите хранить строку в объекте. И не как нормальные люди, а вот нетривиально, как указатель:
class PString {
std::string* ptr;
public:
PString (const std::string& str) : ptr(new std::string(str)) {}
};Там где new, обязательно должен быть delete:
class PString {
std::string* ptr;
public:
PString (const std::string& str) : ptr(new std::string(str)) {}
~PString () {delete ptr;}
}; Так вот правило нам говорит, что раз мы определили деструктор, мы как-то необычно менеджим ресурсы в нашем классе. А если это так, то нам вряд ли подойдет, что сгенерирует компилятор. Более того, компилятор откажется сам неявно генерировать определенные методы. Поэтому нужно вручную определить все 5 специальных методов.
Ну и быстро допишем все:
class PString {
std::string* ptr_ = nullptr;
public:
PString (const std::string& str) : ptr_(new std::string(str)) {}
~PString () {delete ptr_;}
PString (const PString& other) : ptr_(new std::string(*other.ptr_)) {}
PString (PString&& other) noexcept : ptr_(other.ptr_) {ptr_ = nullptr;}
PString& operator=(const PString& other) {
if (this != &other) {
delete ptr_; // free existing resource
ptr_ = new std::string(*other.ptr_); // deep copy
}
return *this;
}
PString& operator=(PString&& x) noexcept {
if (this != &x) {
delete ptr_; // free existing resource
ptr_ = x.ptr_; // move resource and then just like in move constructor
x.ptr_ = nullptr;
}
return *this;
}
}; Мы определили деструктор, копи и мув конструкторы, а также операторы копи и мув присваивания.
Где же конструктор по-умолчанию?
1️⃣ Он не управляет ресурсами, он лишь даем им значение по умолчанию.
2️⃣ Не всегда он даже нужен, а иногда даже вреден
Что будет, если я создам кольцевой буфер по-умолчанию и попытаюсь в него запихать элемент? Будет не то же самое, что и с std::vector, не выделится новая память. В худшем случае будет запись в невыделенную память. В лучшем - будет написана проверка на дурака.
Но зачем эта проверка, когда можно просто не писать конструктор по-умолчанию и избежать ошибок?
То есть для полноценного управления ресурсами достаточно копи/мув операторов, копи/мув конструкторов и деструктора.
Follow the rules. Stay cool.
#design #goodpractice
👍8❤5🔥4