uz
Feedback
Грокаем C++

Грокаем C++

Kanalga Telegram’da o‘tish

Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов. По всем вопросам (+ реклама) @ninjatelegramm Менеджер: @Spiral_Yuri Реклама: https://telega.in/c/grokaemcpp Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat

Ko'proq ko'rsatish
9 388
Obunachilar
+224 soatlar
+147 kunlar
+1330 kunlar
Postlar arxiv
​​Template type deduction #новичкам Пользователи 98-го стандарта недоумевали, почему они обязаны при наличии инициализатора указывать полный тип переменной при ее определении. "Если я еще раз напишу полный тип итератора, то я устрою Роскомнадзор", "Вы что, хотите, чтобы я пальцы стёр?!" и тд. У многих были такие мысли. И это, вообще говоря, было очень странно, потому что компилятор уже на тот момент мог сам выводить тип на основе типа другого выражения! В шаблонах. Вам же не обязательно писать в шаблонных функциях тип шаблонного аргумента. В некоторых случаях компилятор за вас это может сделать.
template <class Container>
size_t my_size(const Container& container)
{
  return container.size();
}

std::cout << my_size(std::vector<int>(10, 0)) << std::endl;
Здесь выведется 10 и, как вы видите, для функции my_size мы не указывали явным образом шаблонный тип. Ну и раз уже есть наработанная схема, к которой разработчики уже привыкли, то почему бы именно ее не использовать в качестве основы вывода типов для auto? Этим риторическим вопросом задались контрибьютеры в 11-й стандарт и теперь у нас действительно есть ключевое слово auto, для которого вывод типов практически ничем не отличается от вывода типов для шаблонных функций! Поэтому надо понимать, что это за зверь такой, чтобы осознанно использовать auto. Чтобы мы все понимали, о чем конкретно будем говорить, посмотрим на следующий псевдокод:
template <class T>
void func(ParamType param) {...}

func(expression);
Все будем разбирать на примере подобной шаблонной функции. Так вот процесс вывода типов ParamType и Т на основании типа выражения expression - это и есть вывод шаблонных типов. Небольшой пример:
template <class T>
size_t my_size(const std::vector<T>& vec) {...}

template <class T>
void fun(const T& param) {...}

my_size(std::vector<int>(10, 0));
int i = 42;
fun(i)
В случае c my_size ParamType - const std::vector<T>&, а тип T - int. В случае с fun ParamType принимает вид типа Т, обвешанного побрякушками, типа const- и ссылочного квалификаторов. Здесь ParamType = const T&, а Т = int. То есть ParamType - все то, что стоит слева от имени шаблонного параметра, и на основе выведенного ParamType уже принимается решение о типе Т. Поэтому очень важно понимать не только, какой тип имеет expression, но и какой вид принимает ParamType. Есть всего 3 мажорных варианта: 1) ParamType - указатель или ссылка, но не универсальная ссылка. 2) ParamType - универсальная ссылка. 3) ParamType - ни указатель, ни ссылка. Все это в следующих постах будем раскрывать подробнее. Use deduction. Stay cool. #cppcore #template

​​Опасности std::unordered_map #опытным Когда писал прошлый пост, я хотел сразу вставить в пример range-based-for, чтобы показать одну приколюху. Но решил, что это заслуживает отдельного поста. В копилку полезности auto. Вдруг вы решили не пользоваться этой фичей и пишите вот так:
struct Customer{
  Customer(int num) : data{num} {}
  Customer(const Customer& other) {
    data = other.data;
    std::cout << "Copy ctor" << std::endl;
  }
private:
  int data;
};
std::unordered_map<std::string, std::vector<Customer>> data;
data["qwe"] = {Customer{1}, Customer{2}};
for (const std::pair<std::string, std::vector<Customer>>& item : data) {
  std::cout << "Idle print" << std::endl;
}
Вроде бы все хорошо и выглядит, как надо. И ожидать мы в консоли будем такой вывод:
Copy ctor
Copy ctor
Idle print
При заполнении вектора кастомеры копируются из временных объектов, вызывается копирующий конструктор с принтом, и далее вывод цикла. Однако на самом деле вывод будет такой:
Copy ctor
Copy ctor
Copy ctor
Copy ctor
Idle print
Мы этого совсем не ожидали. Откуда еще 2 копии?!! Дело в том, что в нашей неупорядоченной мапе хранятся не std::pair<std::string, std::vector<Customer>>, а std::pair<const std::string, std::vector<Customer>>. Это в принципе особенность std::unordered_map: ключ мапы - неизменяемый объект, поэтому обобщенно мапа хранит std::pair<const Key, Value>. И у компилятора не получается забиндить пару с константным ключом к паре с неконстантным. Но делать-то что-то надо. Поэтому он просто делает копию пары, лежащей в мапе, и переменная цикла item ссылается на этот временный объект. Дальше временный объект уничтожается после завершения своей итерации цикла и уходит в историю, как тот, кого не ждали, кто просто так пожрал ресурсы, ничего полезного не сделал и ушел. Осуждаю таких наглецов. Ну и естественно, эта проблема просто решается использованием ключевого слова auto.
struct Customer{
  Customer(int num) : data{num} {}
  Customer(const Customer& other) {
    data = other.data;
    std::cout << "Copy ctor" << std::endl;
  }
private:
  int data;
};
std::unordered_map<std::string, std::vector<Customer>> data;
data["qwe"] = {Customer{1}, Customer{2}};
for (const auto& item : data) {
  std::cout << "Idle print" << std::endl;
}
Теперь у нас есть ожидаемый вывод. Make your life easier. Stay cool. #cpp11 #STL

Офер в Яндекс для опытных бэкендеров за два дня 24–25 августа приглашаем бэкендеров с опытом работы от пяти лет получить офер
Офер в Яндекс для опытных бэкендеров за два дня 24–25 августа приглашаем бэкендеров с опытом работы от пяти лет получить офер в Яндекс через multitrack за 2 дня. Достаточно решить задачи онлайн до 20 августа и пройти несколько технических секции 24 августа, чтобы уже 25-го получить офер и выбрать три команды, к которым вам было бы интересно присоединиться. Как правило, за несколько собеседований сложно понять, подходит ли вам команда и наоборот. Multitrack позволит вам поработать в трёх разных командах Яндекса и выбрать подходящую. Вы сможете погрузиться в рабочие процессы, познакомиться с будущими коллегами и понять, с какими задачами и технологиями хотите работать. Узнать подробности и зарегистрироваться.

Вывод типов #новичкам С++ - статически типизированный язык, что значит, что типы всех объектов должны быть известны на этапе компиляции. Это хорошо для безопасности программы и предсказуемости поведения, но не очень хорошо с точки зрения удобства написания программы. Не всегда мне хочется писать что-то типа "im::so::tired::of::typing::long<types>::iterator". Точнее никогда. Да, есть алиасы и синонимы, это нужные и полезные вещи. Но не на все же гигадлинные типы их вводить. Очень хочется, чтобы работу по "написанию" типов делал кто-то за нас. Ведь в конце концов, все сигнатуры функций и методов известны, нормальные пацаны используют явные плюсовые касты, инициализаторы обычно представляют из себя понятные типы. Да, есть всякие приколы с неявным приведением типов и неэксплисит конструкторами от одного аргумента. Но попробовать-то стоит? Так и подумали создатели С++11 и решили ввести для решения этой проблемы ключевое слово auto. На самом деле они ничего не вводили, а вдохнули новую жизнь в уже существующее ключевое слово. У нас даже пост про это есть. Вещь - суперполезная и нужная. Сохраняет много пальчиковых усилий ленивым разработчикам. Например, у меня есть набор коллекций данных, каждая из которых связана с определенным идентификатором. Этот набор можно описать довольно просто:
std::unordered_map<std::string, std::vector<Customer>> data;
Так вот, чтобы по этой мапе проитерироваться раньше нужно было писать вот так:
for (std::unordered_map<std::string, std::vector<Customer> >::iterator it = data.begin(); it != data.end(); it++) {...}
Это конечно никуда не годится, выглядит ужасно, нечитаемо, да и код повторяется. Теперь подключаем 11-у плюсы и случается магия:
for (auto it = data.begin(); it != data.end(); it++) {...}
А добавив заклинание под называнием range-based-for, получим:
for (const auto& elem: data) {...}
Не идеально, это вам не питон. Но уже ощутимо приятнее и короче раза в 3. Но тут встает вопрос: а как вообще эти типы-то выводятся? Есть наверное какие-то правила, алгоритм, по которому компилятор выводит тип? Есть. Иначе это было бы магией(хотя грустновато без нее в нашем мире). Его можно запомнить довольно легко. Поэтому в нескольких следующих постах мы будем разбирать эту тему. А вообще знаете, что существует 3 вида вывода типов? Может и больше, но 3 точно есть, обещаю) Delegate your work. Stay cool. #cpp11

Всем привет) Мы для вас подготовили серию статей по выводу шаблонных параметров. Когда-то давно нас попросили рассказать про конструкцию decltype(auto), но про это сложно будет рассказывать, не разобрав по отдельности decltype и auto. Но в первую очередь нужно поговорить про вывод типов с плюсах, поэтому начнем с этого. Здесь будет приведен список будущих тем. По мере появления постов здесь будут появляться гиперссылки для более удобного поиска. Также и сами темы могут добавляться по мере выхода статей. Темы: 👉🏿 Вывод типов 👉🏿 Template type deduction 👉🏿 Небольшой пролог для вывода типов 👉🏿 ParamType - не cv-квалифицированная ссылка 👉🏿 ParamType - не cv-квалифицированный указатель 👉🏿 ParamType - cv-квалифицированный параметр 👉🏿 ParamType - универсальная ссылка 👉🏿 ParamType - не ссылка и не указатель 👉🏿 Аргумент шаблонной функции - массив 👉🏿 Аргумент шаблонной функции - функция Возможно где-то вы уже это видели(привет, Скотт!). Однако оттуда мы взяли структуру, наполнение будет переформатировно и расширено.

​​nullptr Вероятно, каждый, кто писал код на C++03, имел удовольствие использовать NULL и постоянно ударяться мизинцем ноги об этот острый уголок тумбочки. Дело в том, что NULL использовался, как обозначение нулевого указателя, который никуда не указывает. Но если он для этого и использовался - это не значит, что он таковым являлся. Да и являлся он котом в мешке. Это макрос, который мог быть определен как 0 aka int zero или 0L aka zero long int, но всегда это вариация интегрального нуля. И уже эти чиселки могли быть приведены к типу указателя. Вот в этом-то и вся проблема. NULL очень явно хочет себя видеть в роли указателя, но по факту в зеркале видит число. Допустим, у нас есть 2 перегрузки одной функции: одна для инта, вторая для указателя:
class Spell { };

void castSpell(Spell* theSpell);
void castSpell(int spellID);

int main() {
  castSpell(NULL);
}
Намерения ясны: мы хотим вызвать перегрузку для указателя. Но это гарантировано не произойдет! В произойдет один из двух сценариев: если NULL определен как 0, то просто без объявления войны в 4 часа утра 22 июня вызовется вторая перегрузка. Если как 0L, то компилятор поругается на неоднозначный вызов: 0L может быть одинаково хорошо сконвертирован и в инт, и в указатель. Проблему можно решить енамами, принимать вместо его вместо инта и передавать для нулевого spellID что-то типа NoSpell. Но надо опять городить огород. Почему все не работает из коробки?! С приходом С++11 начало работать из коробки. Надо только забыть про NULL и использовать nullptr. Ключевое слово nullptr обозначает литерал указателя. Это prvalue типа std::nullptr_t. И nullptr неявно приводится к нулевому значению указателя для любого типа указателя. Это объект отдельного типа, который теперь к простому инту не приводится. Поэтому сейчас этот код отработает как надо:
class Spell {};

void castSpell(Spell* theSpell);
void castSpell(int spellID);

int main() {
  castSpell(nullptr);
}
Так как nullptr - значение конкретного типа std::nullptr_t, то мы может принимать в функции непосредственно этот тип, а не общий тип указателя. Такая штука используется, например, в реализации std::function, конструктор которого имеет перегрузку для std::nullptr_t и делает тоже самое, что и конструктор без аргументов.
/*
*  @brief Default construct creates an empty function call wrapper.
*  @post !(bool)*this
*/
function() noexcept
: _Function_base() { }

/
*  @brief Creates an empty function call wrapper.
*  @post @c !(bool)*this
*/
function(nullptr_t) noexcept
: _Function_base() { }
По той же причине nullptr даже при возврате через функцию может быть приведен к типу указателя. А вот обычные null pointer константны не могут похвастаться таким свойством. Они могут приводиться к указателям только в виде литералов.
template<class T>
constexpr T clone(const T& t)
{
    return t;
}
 
void g(int)
{
    std::cout << "Function g called\n";
}
 
int main()
{
    g(nullptr);        // Fine
    g(NULL)            // Fine
    g(0);              // Fine
 
    g(clone(nullptr)); // Fine
//  g(clone(NULL));    // ERROR: non-literal zero cannot be a null pointer constant
//  g(clone(0));       // ERROR: non-literal zero cannot be a null pointer constant
}
clone(nullptr) вернет тот же nullptr и все будет работать гладко. А для 0 и NULL функция вернет просто int, который сам по себе неявно не конвертится в указатель. Думаю, что вы все и так пользуете nullptr, но этот пост обязан быть на канале. Как говорится "Use nullptr instead of NULL, 0 or any other null pointer constant, wherever you need a generic null pointer." Be a separate subject. Stay cool. #cppcore #cpp11

🦾Хардкорный тест по языку С🦾 📌Пройдите тест из 20 вопросов и проверьте, насколько вы готовы к обучению на углубленном курс
🦾Хардкорный тест по языку С🦾 📌Пройдите тест из 20 вопросов и проверьте, насколько вы готовы к обучению на углубленном курсе - «Программист С» от OTUS. Сможете сдать - пройдете на курс по спеццене! ⏰ Время прохождения теста ограничено 30 минут 👉ПРОЙТИ ТЕСТ

​​Результаты ревью Круто вчера постарались, столько проблем нашли в этом маленьком кусочке кода. Больше всего проблем нашли два подписчика со скрина: я не смог выбрать из них одного, так как их пункты хоть и пересекаются, но все же дополняют друг друга различными мыслями. Давайте похлопаем нашим героям!👏👏👏👏👏👏 А теперь скомпануем все воедино. Напомню, что код был такой:
template<typename T, template<class,class...> class C, class... Args>
std::ostream& operator <<(std::ostream& os, C<T,Args...>& objs)
{
  os << PRETTY_FUNCTION << '\n';
  for (auto obj : objs)
  os << obj << ' ';
  return os;
}
Сначала очевидное и то, что бросается в глаза. Все правильно поняли, что смысл кода - вывести элементы любого контейнера на выходной поток. Всвязи с этим следующие рассуждения: ❗️ Второй аргумент оператора принимается по значению, что ведет с излишним копированиям. Лучше использовать константную ссылку, так как мы не собираемся изменять значения контейнера. ❗️ В цикле тоже будут копирования, так как obj - объект, а не ссылка. Лучше использовать const auto &. ❗️ В первой строчке смешиваются class и typename. Это путает читателя, заставляя задумываться о тайном замысле использования разных ключевых слов. Лучше везде использовать class, так как Артем отметил , что до С++17 в шаблон-шаблонных параметрах нельзя было использовать typename. ❗️ Не очень выразительное название для аргумента, которым предполагается быть контейнеру. Хотя бы полностью написать Container. ❗️ Вывод элементов очень странный и кривой. Как минимум после последнего элемента будет ставиться пробел. Решить проблему можно с помощью стандартного алгоритма std::copy и интересного экспериментального итератора std::experimental::make_ostream_joiner, который может выводить элементы последовательности через разделитель, не записывая разделитель в конце! Выглядит это так:
std::copy(vec.begin(), vec.end(),
  std::experimental::make_ostream_joiner(std::cout, ", "));
На этом очевидные недостатки, которые мог выделить даже не очень разбирающийся в шаблонах читатель, заканчиваются. Посмотрим чуть поглужбе. Функция используется для отладочных и учебных целей. Это понятно по использованию макроса PRETTY_FUNCTION. Он позволяет посмотреть полную сигнатуру функции с расшифровкой всех шаблонных параметров. Он довольно сильно помогает при обучении. Но к сожалению, этот макрос определен только под gcc/clang. Давайте уж не будем сильно внимание заострять на кроссплатформенности и целесообразности использования этой конструкции. В прод функция явно не пойдет. Д. А более интересные и кроссплатформенные варианты вывода сигнатуры функции можно посмотреть тут. Однако автором этот кусок кода преподносился, как универсальный принт контейнеров STL. А вот тут уже залет! Потому что он не только не универсальный, но еще и не безопасный и корявый!. 🔞 Для класса std::string уже определен оператор вывода на поток, поэтому при наличии этого куска в общем коде мы просто не сможем выводить строку, так как компилятор найдет 2 подходящие перегрузки и не сможет из них выбрать лучшую. Можно ограничить тип контейнера с помощью sfinae/концептов. 🔞 Перегрузка не будет работать для мап. У них элементы - пары, которые не имеют собственной реализации вывода на поток. Да и вообще: если элементы "контейнера" не умеют выводиться на поток, то будет ошибка. Выход - поставить sfinae/концепт на существовании перегрузки на поток вывода для типа Т. 🔞 В предыдущем пункте я взял слово контейнер в кавычки. Все потому что сигнатура функции способна принимать любую шаблонную тварь, даже какой-нибудь std::shared_ptr. А для него уже перегружен оператор вывода. Опять компилятор не сможет выбрать из двух одинаковых перегрузок. Поэтому было бы неплохо поставить ограничение на существование методов begin() и end(). ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ Fix your flaws. Stay cool. #template #STL #cppcore #cpp17 #cpp20

Ревью #опытным На просторах интернета нашел интересный сэмпл кода, которым захотел с вами поделиться. Интересный, потому что
Ревью #опытным На просторах интернета нашел интересный сэмпл кода, которым захотел с вами поделиться. Интересный, потому что он очень маленький, но там есть большое пространство для тщательного #ревью. А еще у новичков могут глаза вытечь, посмотрев на первую строчку. В общем, равнодушным никто не останется. Оставляйте в комментах свои замечания и возможные варианты исправления ситуации. Комментарии наиболее продуктивного критика выложу на канал вместе с ответом. Хватит отдыхать, пора ошибки в коде искать! Analyse your life. Stay cool.

​​Странный размер std::unordered_map #опытным Стандартная ситуация. Создаем контейнер, резервируем подходящий размер для ожидаемого количества элементов в коллекции и запихиваем элементы. Все просто. Но это с каким-нибудь вектором все просто. А хэш-мапа - дело нетривиальное. Смотрим на код:
constexpr size_t map_size = 6;
std::unordered_map<int, int> mymap;
mymap.reserve(map_size);
for (int i = 0; i < map_size; i++) {
  mymap[i] = i;
}
std::cout << "mymap has " << mymap.bucket_count() << " buckets\n";
Все, как обычно. А теперь вывод:
mymap has 7 buckets
WTF? Я же сказал выделить в мапе 6 бакетов, а не 7. Какой непослушный компилятор! Вообще, поведение странное, но может там просто всегда +1 по какой-то причине? Поменяем map_size на 9 и посмотрим вывод:
mymap has 11 buckets
Again. WTF? Уже на 2 разница. Нужна новая гипотеза... Попробуем третье число. Возьмем 13.
mymap has 13 buckets
А тут работает! Но это не прибавляет понимания проблемы... В чем же дело? Из цппреференса про метод reserve:
Request a capacity change

Sets the number of buckets in the container (bucket_count) to the most appropriate to contain at least n elements.
То есть стандарт разрешает реализациям выделять больше элементов для мапы, чем мы запросили. Легитимацию безобразия мы получили, но хотелось бы внятное объяснение причины предоставления такой возможности. Реализации обычно выбирают bucket_count исходя из соображений быстродействия(как обычно). Тут они выбирают из двух опций: 1️⃣ Выбирают в качестве bucket_count степень двойки, то есть округляют до степени двойки в большую сторону. Это помогает эффективно маппить результат хэш функции на размер самой хэш-таблицы. Можно просто сделать битовое И и отбросить все биты, старше нашей степени. Что делается на один цикл цпу. Но этот способ имеет негативный эффект в виде того же отбрасывания битов. То есть эти страшие биты никак не влияют на маппинг хэша на бакеты, то уменьшает равномерность распределения. Таким способом пользуется Visual C++. 2️⃣ Поддерживают bucket_count простым числом. Это дает крутой эффект того, что старшие биты также влияют на распределение объектов по бакетам. В этом случае даже плохие хэш-функции имеют более равномерное размещение бакетов. Однако наивная реализация такого подхода заставляет каждый раз делить на рантаймовое значение bucket_count, что может занимать до 100 раз больше циклов. Более быстрой альтернативой может быть использование захардкоженой таблицы простых чисел. Индекс в ней выбирается на основе запрашиваемого значения bucket_count. Таким образом компилятор может заоптимизировать деление по модулю через битовые операции, сложения, вычитания и умножения. Можете посмотреть на эти оптимизации более подробно на этом примере в годболт. Этой реализацией пользуется GCC и Clang. Вот такие страсти происходят у нас под носом под капотом неупорядоченной мапы. Optimize everything. Stay cool. #STL #optimization #compiler

«Мне нравится обучать программированию и информатике»: пост для репетиторов и тех, кто хоть раз задумывался ими стать Все мы привыкли воспринимать репетиторство, как хобби или подработку. На самом деле, каждый из наших учеников и подписчиков может сильно преуспеть в этой сфере и сделать её основной статьей дохода. И, самое главное, сделать вклад в будущее школьников и студентов, которые только в начале своего пути в IT. Рекомендуем прочитать эту статью👉: https://t.me/mishaberezovoy Автор статьи Михаил Березовой. Студент факультета компьютерных наук ВШЭ, олимпиадник. За 5 лет репетиторства он разработал систему, которой пользуются все начинающие репетиторы, а действующие с её помощью сокращают время работы и увеличивают доход. Его советам точно можно доверять, читайте даже не задумываясь. ЗАБРАТЬ СТАТЬЮ

Jobski - твой помощник при поиске работы в IT Сервис индивидуально подбирает вакансии, учитывая ваш опыт, навыки, стек технол
Jobski - твой помощник при поиске работы в IT Сервис индивидуально подбирает вакансии, учитывая ваш опыт, навыки, стек технологий. Узнать больше #реклама jobski.ru О рекламодателе

​​Переобуваемся В прошлом посте я сказал, что лигитимно создавать вектор констант. Ну и конечно я не просто так написал весь этот пост от балды, все проверял на своей машинке. Хз, что в голове было у компилятора, но он пропускал вектор констант. Больше не буду полагаться на эту шайтан-машину. Да простит Бог его душу, а мы сейчас поправим то, что было написано вчера. Нормальные компиляторы не соберут вам программу с вектором констант, потому что существуют ограничения, наложенные на аллокаторы. Стандартный вектор объявляется вот так:
template<  
    class T,  
    class Allocator = std::allocator<T>  
> class vector;
Заметим, что шаблонный тип аллокатора совпадает с шаблонным типом элементов вектора. То есть мы будем инстанцировать аллокатор с тем же шаблонным параметром, что и элементы вектора. А на все аллокаторы, которые могут работать со стандартной библиотекой, наложены ограничения. Одно из них гласит, что шаблонный параметр Т аллокатора должен быть cv-unqualified типом. То есть константные типы туда не входят. Ну вот собственно и все. Сам контейнер здесь действительно не при чем, ограничения заложены в аллокатор. Спасибо Игорю за то, что подметил ошибку в посте. Если вы все-таки рьяно хотите вектор констант, то можете рассмотреть варианты оборачивания элементов в умные указатели:
std::vector<std::unique_ptr<const Type>> vec;
Тогда шаблонный тип, с которым инстанцируется вектор и аллокатор, будет неконстантным и ограничения влиять не будут. И вы можете любые операции с этим вектором делать: хоть сортировки, хоть вставку посередине. В том числе для таких ситуаций в нашем коммьюнити находятся крутые специалисты. Когда написание постов поставлено на поток, то ошибки неизбежны. Да они и в принципе неизбежны, мы тоже люди и многого не знаем. Иногда еще и инструментарий подводит. И это замечательно, что у нас в канале есть люди, которые вдумчиво читают посты и могут дать адекватную критику по фактам. От этого выигрывают все: критики экологично повышают свою значимость с своих глазах и глазах подписчиков, а в коммьюнити не пропускается ошибочная информация. Не бойтесь делать ошибки, они уменьшают объем вашего незнания. Make mistakes. Stay cool. #cppcore

​​Вектор констант В прошлый раз мы рассмотрели вектор ссылок. А давайте чуть углубимся сюда и посмотрим, как будет себя вести вектор констант. Константные объекты уже удовлетворяют требованию Erasable. Для них либо определен деструктор(пользовательские объекты), либо это константные тривиальные типы, которые тоже Erasable. Казалось бы на этом можно закончить пост, можно создавать и ладно, много бубнить не нужно об этом. Но вот при использовании этой сущности могут возникать интересные эффекты.
struct A {
  A(int num) : a{num} {}
  int a;
};

std::vector<const A> vec;

for (int i = 0; i < 5; i++) {
  vec.push_back(i);
}

for (int i = 0; i < 5; i++) {
  std::cout << vec[i].a << std::endl;
}
Ничего сверхъестественного. Просто создаем вектор, заполняем его и выводим. Что может пойти не так? Это дело не соберется на методе push_back. Тип А - тривиально копируемый, что не допускается при вызове этого метода. Но как только мы добавим нетривиальный деструктор или конструктор копирования - все заработает нормально. Этот же факт значит, что для любых тривиальных типов вы не сможете добавлять так элементы в вектор констант. Не знаю, какие рассуждения лежат за этим, знающие могут оставить свои мысли в комментах. Но это ладно. Дальше мы хотим поработать с этим вектором и, например, отсортировать его.
struct A {
  A(int num) : a{num} {}
  ~A() {} // Important here
  int a;
};

std::vector<const A> vec;

for (int i = 0; i < 5; i++) {
  vec.push_back(i);
}

std::sort(vec.begin(), vec.end())
И тоже натыкаемся на ошибку компиляции. Внутри себя std::sort использует std::swap, которая меняет значения своих операндов inplace. Это значит, что мы должны иметь возможность присваивать объектам другие данные. А для константных объектов сделать это будет очень проблематично. Метод erase также нерабочий из-за отсутствия возможности присваивания. erase позволяет удалять элементы из середины вектора. Для этого придется "сдвигать" все элементы справа от удаляемых, чтобы заполнить пустоту. Делается это либо перемещением, либо копированием. Но для константных объектов очевидно запрещено вызывать оператор присваивания. И хотя, интерфейс такого контейнера будет ограничен, мы все равно можем использовать читающие алгоритмы над ним. Например, подсчитывать какую-нибудь статистику по элементам. Также мы можем в рантайме свободно добавлять и удалять элементы из контейнера. Через emplace_back и pop_back. И это выгодно выделяет вектор констант на фоне константного вектора. Вы не хотите изменять сами элементы, но хотите иметь возможность изменять их множество и выполнять различные читающие операции над ним. Именно для этих задач и подходит вектор констант. Главное - аккуратнее с интерфейсом) Be careful. Stay cool.

☝️Для программистов важно понимать устройство и организацию оперативной памяти — от этого зависит надёжность и производительн
☝️Для программистов важно понимать устройство и организацию оперативной памяти — от этого зависит надёжность и производительность кода. Особенно критично это для тех, кто пишет на C, где есть уйма возможностей для ручного управления памятью: malloc, jemalloc, tcmalloc, mimalloc... 👉Узнайте больше на бесплатном авторском вебинаре Дмитрия Кириллова «Написание расширения PostgreSQL на языке С»: регистрация На уроке узнаете: - как устроена оперативная память на уровне операционной системы - что происходит при динамическом распределении памяти (и зачем нужны специализированные аллокаторы) - как грамотно управлять памятью, чтобы повысить качество своего кода 🤝Понравится вебинар — продолжите обучение на онлайн-курсе «Программист С» по специальной цене! erid: LjN8Jsusp

​​Вектор ссылок #опытным Не знаю, задумывались ли вы когда-нибудь создать вектор ссылок. Наверное задумывались, но не прям, чтобы пытались воплотить в жизнь. Не очень понятны кейсы применения этих сущностей. Однако они довольно хорошо подсвечивают одну интересную и базовую особенность вектора. Дело в том, что вы не можете создать вектор ссылок. Не можете и все. Попробуйте написать что-то такое и запустить сборку:
std::vector<int&> vec;
Вылезет какая-то совершенно монструозная кракозябра, по которой мы хрен пойми, что должны понять. Это немного камней в огород бесполезных сообщений об ошибках в плюсах, но продолжим. В сущности это происходит по одной причине. Шаблонный тип vec не удовлетворяет требованиям к типам элементов вектора. До C++11 и появления мув-семантики элементы вектора должны были удовлетворять требованиям CopyAssignable и CopyConstructible. То есть из этих объектов должны получаться валидные копии, притом что исходный объект оказывается нетронутым. Это условие, кстати, не выполняется для запрещенного в РФ иноагента std::auto_ptr. Так вот ссылочный тип - не CopyAssignable. При попытке присвоить ссылке что-то копирования не происходит, а происходит просто перенаправление ссылки на другой объект. После С++11 требования немного смягчились и теперь единственный критерий, которому тип элементов вектора должен удовлетворять - Erasable. Но ссылки также не попадают под этот критерий(для них не определен деструктор). Поэтому сидим без вектора ссылок. Или нет? Можно хакнуть этот ваш сиплюсплюс и создать вектор из std::reference_wrapper. Это такая тривиальная обертка над ссылками, чтобы ими можно было оперировать, как обычными объектами. В смысле наличия у них всех специальных методов классов. Но будьте осторожны(!), потому что есть одна большая проблема со ссылками. Вот мы создали и заполнили контейнер ссылками на какие-то объекты. И потом вышли из скоупа, где были объявлены объекты, на которые ссылки указывают. Вектор есть, ссылки есть, а объектов нет. Это чистой воды undefined behavior. Ссылки будут указывать на уже удаленные объекты. Пример:
std::vector<std::reference_wrapper<int>> vec;
int * p = nullptr;
{
  int i;
  for (i = 0, p = &i; i < 5; i++) {
    vec.emplace_back(i);
  }
}

*p = 10;

for (int i = 0; i < 5; i++) {
  std::cout << vec[i] << std::endl;
}
Вывод будет такой:
10
10
10
10
10
Подумайте пару секунд, почему так. Переменная i меняется и мы добавляем ссылки на эту переменную в вектор. По итогу все элементы вектора указывают на одну и ту же переменную. Поэтому и элементы все одинаковы. Но раз ссылка - это обертка над указателем, то элементы вектора по факту хранят адрес того места, где была переменная i. Поэтому все изменения ячейки памяти этой переменной будут отражаться на ссылках, даже если переменная уже удалена. Вот мы и сделали грязь: сохранили адрес ячейки и изменили его после выхода из скоупа цикла и удаления переменной i. Так обычно и происходит на стеке: переменная кладется на стек, с ней работают, она удаляется при выходе из скоупа и потом другие объект занимают место удаленной переменной в памяти. Мы здесь сымитировали такой процесс. Так как вектор после выхода из скоупа цикла хранит висячие ссылки, то поведение в такой ситуации неопределено и наш грязный мув четко это показывает. После присваивания нового значения по указателю p все ссылки будут иметь то же самое значение. Хотя изначально такая ситуация вообще не предполагалась. Будьте аккуратны со ссылками. В этом случае проще использовать какой-нибудь умный указатель. Все будет чинно и цивильно. И никакого UB. Be careful. Stay cool. #cpp11 #cppcore #STL

​​Swap idiom. Pros and cons #опытным В этом посте поговорили про суть swap идиомы. Сегодня обсудим ее плюсы и минусы. Плюсы вроде как обсуждали, но я финализирую, когда можно рассмотреть внедрение swap idiom: ✅ Если у вас конструктор копирования может бросить исключение и вы можете написать небросающую функцию swap. Тогда за счет того, что захват ресурсов(копирование или перемещение во временный объект параметра функции) происходит до модификации текущего объекта, то мы получаем строгую гарантию безопасности исключений при работе с присваиванием объектов. ✅ Если вы хотите красивый, лаконичный и понятный код без повторений действий. ✅ Вы не очень беспокоитесь о потенциальных потерях производительности. Погнали по минусам: ❗️ Не всегда можно написать nothrowing swap. Для базовых типов и указателей - да. Но swap нетривиальных типов использует временный объект. При создании которого и может возникнуть исключение. Сейчас swap делается с помощью перемещающих операций, но например в С++03 std::string мог кинуть исключение в копирующем конструкторе. Да и сейчас поля класса могут быть немувабельными и бросающими при копировании. Это надо иметь ввиду. ❗️ Каждый раз при присваивании мы выполняем 2 операции: конструктор копирования + swap или конструктор перемещения + swap. "Потери производительности" надо конечно тестить и смотреть реальные результаты, но в голове все равно надо держать потенциальные просадки. ❗️ Самостоятельно писать деструктор для менеджинга ресурсов в 2к24 - такая себе практика в большинстве случаев. Давно есть std::unique_ptr<T[]>, указатели с кастомными делитерами и прочие вещи. Одно из ключевых преимуществ идиомы - сокращение и переиспользование кода. Так вот с отсутствием деструктора вам вообще может не понадобится кастомное присваивание и вы сможете объявить операции дефолтными, поэтому надобность в идиоме сама по себе отпадет. ❗️❗️ Часто пропускаемый огромный минус: технически у нас есть оператор перемещения, который может принимать rvalue ссылки. Однако мы явным образом не реальзовывали присваивание перемещением, поэтому по правилу 5, компилятор не будет его генерировать за нас и у класса просто будет отсутствовать оператор присваивания перемещением. И хоть текущий класс мы можем мэнэджить без присваивания перемещением, то ситуация изменится, когда мы сделаем текущий класс полем другого. Тогда у этого другого класса не будет генерироваться дефолтный оператор присваивания перемещением! Для его генерации все поля должны иметь такие операторы. А в нашем классе его нет. Это значит, что по дефолту будет использоваться копирующее присваивания и все остальные поля нового класса будут копироваться. А вы об этом даже не знали! И получили жесткую просадку и, потенциально, некорректную логику.
struct FirstField {
  FirstField() = default;
  FirstField(const FirstField& other) {
    std::cout << "FirstField Copy ctor" << std::endl;
  }
  FirstField& operator=(FirstField other) {
    std::cout << "FirstField assign" << std::endl;
    return *this;
  }
  FirstField(FirstField&& other) {
    std::cout << "FirstField Move ctor" << std::endl;
  }
};

struct SecondField {
  SecondField() = default;
  SecondField(const SecondField& other) {
    std::cout << "SecondField Copy ctor" << std::endl;
  }
  SecondField& operator=(const SecondField& other) {
    std::cout << "SecondField Copy assign" << std::endl;
    return *this;
  }
  SecondField(SecondField&& other) {
    std::cout << "SecondField Move ctor" << std::endl;
  }
  SecondField& operator=(SecondField&& other) {
    std::cout << "SecondField Copy assign" << std::endl;
    return *this;
  }
};

struct Wrapper {
  FirstField ff;
  SecondField sf;
};

Wrapper w;
w = std::move(Wrapper{});

// OUTPUT:
// FirstField Move ctor
// FirstField assign
// SecondField Copy assign
Выбор использовать или не исопльзовать - как всегда за вам. Тестируйте гипотезы и выбирайте из них лучшую. Analyse your solutions. Stay cool. #cppcore #cpp11

Repost from N/a
Привет! Меня зовут Бекхан, мне 28 лет. Узнайте обо мне больше, открыв картинку над постом или прочитав полный текст здесь. Се
Привет! Меня зовут Бекхан, мне 28 лет. Узнайте обо мне больше, открыв картинку над постом или прочитав полный текст здесь. Сейчас я занимаюсь разработкой собственной игры с нуля и сталкиваюсь с различными вызовами и подводными камнями. Все свои знания и опыт я конспектирую и делюсь ими на своем сайте и телеграм-канале. Я всегда стараюсь глубоко и основательно разбираться в возникающих вопросах, и мне кажется, что это будет полезно и для вас. Хотя постов в моем телеграм-канале пока не так много, я уверен, что с увеличением аудитории у меня будет больше мотивации делиться своим опытом и писать новые посты. Подписывайтесь на мой телеграм-канал Bekhan Code, чтобы не пропустить полезные советы и инсайты по разработке игр. Попасть в Bekhan Code

Ответ Буду описывать мои мысли, которые мне приходили в голову при решении. Самое базовое, что надо понимать: если есть всего одно яйцо, то стратегия всегда одна - идти снизу вверх и подряд с каждого этажа скидывать, пока яйцо не разобьется. Число 2 как бы намекает на то, что нужно что-то уполовинить. Но если мы сбросим с 50-го этажа и яйцо разобъется, это нам не сильно сократит задачу - придется в худшем случае еще 49 раз бросить. Но можно шаги по-другому уполовинить - ходить через один этаж. Как только первое яйцо разобьется, можно пойти на этаж ниже и сбросить оттуда второе яйцо. Так мы точно определим нужный этаж. В этом случае мы гарантировано найдем нужный этаж за 51 бросок. Той же логикой можно увеличивать шаг - идти через 3/4/5 и тд этажей. Тогда после того, как первое яйцо разобьется, мы пойдем с предыдущего посещенного этажа вверх и будем подряд бросать. Формула для нахождения гарантированного количества шагов c помощью такого методв - (100 // step) + (step - 1). Путем нехитрых математико-алгебраических вычислений придем к тому, что оптимальный вариант - идти через 10 этажей. Да и число красивое. В этом случае мы сможем найти нужный этаж за 19 бросков. Но оптимальное ли это решение? Вообще говоря, нам очень нравится ходить через много этажей. Но не нравится потом много раз бросать после первого разбитого яйца. Если не привязываться к гарантированности, то для малых N нам выгодно ходить с большим шагом. Потому что на поиски рэнджа этажей нам потребуется немного бросков. И этот фактор становится все менее важным, с увеличением N. Можно было бы как-то соединить: ходить в начале с большим шагом, но с каждым броском первого яйца его уменьшать, пока нам вообще второе яйцо не понадобится или пока не наступит 100 этаж. Так можно и соединить. Давайте с каждым броском первого яйца уменьшать шаг на 1 этаж. И к сотому этажу пусть у нас шаг уменьшится до минимума. Получается, что нам нужно начинать с шага в 14. После того, как бросили с 14-го, идем на 27. Потом на 39. И так далее. Прикол в чем - нам всегда нужно будет максимум 14 бросков для нахождения нужного этажа. Это и является ответом. А я говорил, что задача красивая) Solve your problems. Stay cool.

Разбей пару яиц Попалась на глаза интересная логическая задача, решил с вами тут поделиться. Мне понравилось, что в ней нет к
Разбей пару яиц Попалась на глаза интересная логическая задача, решил с вами тут поделиться. Мне понравилось, что в ней нет какого-то твиста или секретного приема, не зная которые до ответа не дойти. Все решается очень плавно, даже как будто бы итеративно. Условие: у вас есть 2 яйца и 100 этажный дом. Яйца у вас очень крепкие(мы же плюсовики), поэтому если их сбрасывать из окон этого дома, то они не будут биться. Однако рано или поздно физика победит и, начиная с какого-то номера этажа N, яйца все-таки будут разбиваться. Вы можете перемещаться в доме вверх и вниз в любой последовательности. Какое минимальное количество бросков гарантировано понадобится, чтобы установить нужный этаж под номером N? Знакомых с решением, прошу воздержаться от комментариев. Всех остальных призываю к обсуждению решения. Уверен, оно вам понравится своей красотой) Ответ будет, как всегда вечером. Хватит яйца мять, пора их разбивать! Challenge yourself. Stay cool. #задачки