Ответ
Буду описывать мои мысли, которые мне приходили в голову при решении.
Самое базовое, что надо понимать: если есть всего одно яйцо, то стратегия всегда одна - идти снизу вверх и подряд с каждого этажа скидывать, пока яйцо не разобьется.
Число 2 как бы намекает на то, что нужно что-то уполовинить. Но если мы сбросим с 50-го этажа и яйцо разобъется, это нам не сильно сократит задачу - придется в худшем случае еще 49 раз бросить.
Но можно шаги по-другому уполовинить - ходить через один этаж. Как только первое яйцо разобьется, можно пойти на этаж ниже и сбросить оттуда второе яйцо. Так мы точно определим нужный этаж. В этом случае мы гарантировано найдем нужный этаж за 51 бросок.
Той же логикой можно увеличивать шаг - идти через 3/4/5 и тд этажей. Тогда после того, как первое яйцо разобьется, мы пойдем с предыдущего посещенного этажа вверх и будем подряд бросать. Формула для нахождения гарантированного количества шагов c помощью такого методв - (100 // step) + (step - 1) - 1.
Путем нехитрых математико-алгебраических вычислений придем к тому, что оптимальный вариант - идти через 10 этажей. Да и число красивое. В этом случае мы сможем найти нужный этаж за 18 бросков.
Но оптимальное ли это решение?
Вообще говоря, нам очень нравится ходить через много этажей. Но не нравится потом много раз бросать после первого разбитого яйца.
Если не привязываться к гарантированности, то для малых N нам выгодно ходить с большим шагом. Потому что на поиски рэнджа этажей нам потребуется немного бросков. И этот фактор становится все менее важным, с увеличением N.
Можно было бы как-то соединить: ходить в начале с большим шагом, но с каждым броском первого яйца его уменьшать, пока нам вообще второе яйцо не понадобится или пока не наступит 100 этаж.
Так можно и соединить. Давайте с каждым броском первого яйца уменьшать шаг на 1 этаж. И к сотому этажу пусть у нас шаг уменьшится до минимума. Получается, что нам нужно начинать с шага в 14. После того, как бросили с 14-го, идем на 27. Потом на 39. И так далее.
Прикол в чем - нам всегда нужно будет максимум 14 бросков для нахождения нужного этажа.
Это и является ответом.
А я говорил, что задача красивая)
Solve your problems. Stay cool.
Буду описывать мои мысли, которые мне приходили в голову при решении.
Самое базовое, что надо понимать: если есть всего одно яйцо, то стратегия всегда одна - идти снизу вверх и подряд с каждого этажа скидывать, пока яйцо не разобьется.
Число 2 как бы намекает на то, что нужно что-то уполовинить. Но если мы сбросим с 50-го этажа и яйцо разобъется, это нам не сильно сократит задачу - придется в худшем случае еще 49 раз бросить.
Но можно шаги по-другому уполовинить - ходить через один этаж. Как только первое яйцо разобьется, можно пойти на этаж ниже и сбросить оттуда второе яйцо. Так мы точно определим нужный этаж. В этом случае мы гарантировано найдем нужный этаж за 51 бросок.
Той же логикой можно увеличивать шаг - идти через 3/4/5 и тд этажей. Тогда после того, как первое яйцо разобьется, мы пойдем с предыдущего посещенного этажа вверх и будем подряд бросать. Формула для нахождения гарантированного количества шагов c помощью такого методв - (100 // step) + (step - 1) - 1.
Путем нехитрых математико-алгебраических вычислений придем к тому, что оптимальный вариант - идти через 10 этажей. Да и число красивое. В этом случае мы сможем найти нужный этаж за 18 бросков.
Но оптимальное ли это решение?
Вообще говоря, нам очень нравится ходить через много этажей. Но не нравится потом много раз бросать после первого разбитого яйца.
Если не привязываться к гарантированности, то для малых N нам выгодно ходить с большим шагом. Потому что на поиски рэнджа этажей нам потребуется немного бросков. И этот фактор становится все менее важным, с увеличением N.
Можно было бы как-то соединить: ходить в начале с большим шагом, но с каждым броском первого яйца его уменьшать, пока нам вообще второе яйцо не понадобится или пока не наступит 100 этаж.
Так можно и соединить. Давайте с каждым броском первого яйца уменьшать шаг на 1 этаж. И к сотому этажу пусть у нас шаг уменьшится до минимума. Получается, что нам нужно начинать с шага в 14. После того, как бросили с 14-го, идем на 27. Потом на 39. И так далее.
Прикол в чем - нам всегда нужно будет максимум 14 бросков для нахождения нужного этажа.
Это и является ответом.
А я говорил, что задача красивая)
Solve your problems. Stay cool.
👍28🔥10❤5⚡1
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, компилятор не будет его генерировать за нас и у класса просто будет отсутствовать оператор присваивания перемещением.
И хоть текущий класс мы можем мэнэджить без присваивания перемещением, то ситуация изменится, когда мы сделаем текущий класс полем другого. Тогда у этого другого класса не будет генерироваться дефолтный оператор присваивания перемещением! Для его генерации все поля должны иметь такие операторы. А в нашем классе его нет.
Это значит, что по дефолту будет использоваться копирующее присваивания и все остальные поля нового класса будут копироваться. А вы об этом даже не знали! И получили жесткую просадку и, потенциально, некорректную логику.
Выбор использовать или не исопльзовать - как всегда за вам. Тестируйте гипотезы и выбирайте из них лучшую.
Analyse your solutions. Stay cool.
#cppcore #cpp11
#опытным
В этом посте поговорили про суть 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
👍17🔥5❤4⚡1
Вектор ссылок
#опытным
Не знаю, задумывались ли вы когда-нибудь создать вектор ссылок. Наверное задумывались, но не прям, чтобы пытались воплотить в жизнь. Не очень понятны кейсы применения этих сущностей. Однако они довольно хорошо подсвечивают одну интересную и базовую особенность вектора.
Дело в том, что вы не можете создать вектор ссылок. Не можете и все. Попробуйте написать что-то такое и запустить сборку:
Вылезет какая-то совершенно монструозная кракозябра, по которой мы хрен пойми, что должны понять. Это немного камней в огород бесполезных сообщений об ошибках в плюсах, но продолжим.
В сущности это происходит по одной причине. Шаблонный тип
До C++11 и появления мув-семантики элементы вектора должны были удовлетворять требованиям CopyAssignable и CopyConstructible. То есть из этих объектов должны получаться валидные копии, притом что исходный объект оказывается нетронутым. Это условие, кстати, не выполняется для запрещенного в РФ иноагента std::auto_ptr. Так вот ссылочный тип - не CopyAssignable. При попытке присвоить ссылке что-то копирования не происходит, а происходит просто перенаправление ссылки на другой объект.
После С++11 требования немного смягчились и теперь единственный критерий, которому тип элементов вектора должен удовлетворять - Erasable. Но ссылки также не попадают под этот критерий(для них не определен деструктор). Поэтому сидим без вектора ссылок. Или нет?
Можно хакнуть этот ваш сиплюсплюс и создать вектор из std::reference_wrapper. Это такая тривиальная обертка над ссылками, чтобы ими можно было оперировать, как обычными объектами. В смысле наличия у них всех специальных методов классов.
Но будьте осторожны(!), потому что есть одна большая проблема со ссылками. Вот мы создали и заполнили контейнер ссылками на какие-то объекты. И потом вышли из скоупа, где были объявлены объекты, на которые ссылки указывают. Вектор есть, ссылки есть, а объектов нет. Это чистой воды undefined behavior. Ссылки будут указывать на уже удаленные объекты. Пример:
Вывод будет такой:
Подумайте пару секунд, почему так. Переменная i меняется и мы добавляем ссылки на эту переменную в вектор. По итогу все элементы вектора указывают на одну и ту же переменную. Поэтому и элементы все одинаковы.
Но раз ссылка - это обертка над указателем, то элементы вектора по факту хранят адрес того места, где была переменная i. Поэтому все изменения ячейки памяти этой переменной будут отражаться на ссылках, даже если переменная уже удалена. Вот мы и сделали грязь: сохранили адрес ячейки и изменили его после выхода из скоупа цикла и удаления переменной i. Так обычно и происходит на стеке: переменная кладется на стек, с ней работают, она удаляется при выходе из скоупа и потом другие объект занимают место удаленной переменной в памяти. Мы здесь сымитировали такой процесс.
Так как вектор после выхода из скоупа цикла хранит висячие ссылки, то поведение в такой ситуации неопределено и наш грязный мув четко это показывает. После присваивания нового значения по указателю
Будьте аккуратны со ссылками. В этом случае проще использовать какой-нибудь умный указатель. Все будет чинно и цивильно. И никакого UB.
Be careful. Stay cool.
#cpp11 #cppcore #STL
#опытным
Не знаю, задумывались ли вы когда-нибудь создать вектор ссылок. Наверное задумывались, но не прям, чтобы пытались воплотить в жизнь. Не очень понятны кейсы применения этих сущностей. Однако они довольно хорошо подсвечивают одну интересную и базовую особенность вектора.
Дело в том, что вы не можете создать вектор ссылок. Не можете и все. Попробуйте написать что-то такое и запустить сборку:
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
👍36🔥14❤8⚡2😁1
Вектор констант
В прошлый раз мы рассмотрели вектор ссылок. А давайте чуть углубимся сюда и посмотрим, как будет себя вести вектор констант.
Константные объекты уже удовлетворяют требованию Erasable. Для них либо определен деструктор(пользовательские объекты), либо это константные тривиальные типы, которые тоже Erasable.
Казалось бы на этом можно закончить пост, можно создавать и ладно, много бубнить не нужно об этом. Но вот при использовании этой сущности могут возникать интересные эффекты.
Ничего сверхъестественного. Просто создаем вектор, заполняем его и выводим. Что может пойти не так?
Это дело не соберется на методе push_back. Тип А - тривиально копируемый, что не допускается при вызове этого метода. Но как только мы добавим нетривиальный деструктор или конструктор копирования - все заработает нормально. Этот же факт значит, что для любых тривиальных типов вы не сможете добавлять так элементы в вектор констант. Не знаю, какие рассуждения лежат за этим, знающие могут оставить свои мысли в комментах.
Но это ладно. Дальше мы хотим поработать с этим вектором и, например, отсортировать его.
И тоже натыкаемся на ошибку компиляции. Внутри себя std::sort использует std::swap, которая меняет значения своих операндов inplace. Это значит, что мы должны иметь возможность присваивать объектам другие данные. А для константных объектов сделать это будет очень проблематично.
Метод erase также нерабочий из-за отсутствия возможности присваивания. erase позволяет удалять элементы из середины вектора. Для этого придется "сдвигать" все элементы справа от удаляемых, чтобы заполнить пустоту. Делается это либо перемещением, либо копированием. Но для константных объектов очевидно запрещено вызывать оператор присваивания.
И хотя, интерфейс такого контейнера будет ограничен, мы все равно можем использовать читающие алгоритмы над ним. Например, подсчитывать какую-нибудь статистику по элементам. Также мы можем в рантайме свободно добавлять и удалять элементы из контейнера. Через emplace_back и pop_back. И это выгодно выделяет вектор констант на фоне константного вектора. Вы не хотите изменять сами элементы, но хотите иметь возможность изменять их множество и выполнять различные читающие операции над ним. Именно для этих задач и подходит вектор констант. Главное - аккуратнее с интерфейсом)
УПД: В комментах указали на критические неточности в посте, завтра будет опровержение
Be careful. Stay cool.
В прошлый раз мы рассмотрели вектор ссылок. А давайте чуть углубимся сюда и посмотрим, как будет себя вести вектор констант.
Константные объекты уже удовлетворяют требованию 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.
❤13👍10🔥4⚡3
Переобуваемся
В прошлом посте я сказал, что лигитимно создавать вектор констант. Ну и конечно я не просто так написал весь этот пост от балды, все проверял на своей машинке. Хз, что в голове было у компилятора, но он пропускал вектор констант. Больше не буду полагаться на эту шайтан-машину. Да простит Бог его душу, а мы сейчас поправим то, что было написано вчера.
Нормальные компиляторы не соберут вам программу с вектором констант, потому что существуют ограничения, наложенные на аллокаторы. Стандартный вектор объявляется вот так:
Заметим, что шаблонный тип аллокатора совпадает с шаблонным типом элементов вектора. То есть мы будем инстанцировать аллокатор с тем же шаблонным параметром, что и элементы вектора.
А на все аллокаторы, которые могут работать со стандартной библиотекой, наложены ограничения. Одно из них гласит, что шаблонный параметр Т аллокатора должен быть cv-unqualified типом. То есть константные типы туда не входят.
Ну вот собственно и все. Сам контейнер здесь действительно не при чем, ограничения заложены в аллокатор. Спасибо Игорю за то, что подметил ошибку в посте.
Если вы все-таки рьяно хотите вектор констант, то можете рассмотреть варианты оборачивания элементов в умные указатели:
Тогда шаблонный тип, с которым инстанцируется вектор и аллокатор, будет неконстантным и ограничения влиять не будут. И вы можете любые операции с этим вектором делать: хоть сортировки, хоть вставку посередине.
В том числе для таких ситуаций в нашем коммьюнити находятся крутые специалисты. Когда написание постов поставлено на поток, то ошибки неизбежны. Да они и в принципе неизбежны, мы тоже люди и многого не знаем. Иногда еще и инструментарий подводит. И это замечательно, что у нас в канале есть люди, которые вдумчиво читают посты и могут дать адекватную критику по фактам. От этого выигрывают все: критики экологично повышают свою значимость с своих глазах и глазах подписчиков, а в коммьюнити не пропускается ошибочная информация.
Не бойтесь делать ошибки, они уменьшают объем вашего незнания.
Make mistakes. Stay cool.
#cppcore
В прошлом посте я сказал, что лигитимно создавать вектор констант. Ну и конечно я не просто так написал весь этот пост от балды, все проверял на своей машинке. Хз, что в голове было у компилятора, но он пропускал вектор констант. Больше не буду полагаться на эту шайтан-машину. Да простит Бог его душу, а мы сейчас поправим то, что было написано вчера.
Нормальные компиляторы не соберут вам программу с вектором констант, потому что существуют ограничения, наложенные на аллокаторы. Стандартный вектор объявляется вот так:
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
🔥45👍16❤5
Странный размер std::unordered_map
#опытным
Стандартная ситуация. Создаем контейнер, резервируем подходящий размер для ожидаемого количества элементов в коллекции и запихиваем элементы. Все просто. Но это с каким-нибудь вектором все просто. А хэш-мапа - дело нетривиальное. Смотрим на код:
Все, как обычно. А теперь вывод:
WTF? Я же сказал выделить в мапе 6 бакетов, а не 7. Какой непослушный компилятор!
Вообще, поведение странное, но может там просто всегда +1 по какой-то причине?
Поменяем map_size на 9 и посмотрим вывод:
Again. WTF? Уже на 2 разница. Нужна новая гипотеза... Попробуем третье число. Возьмем 13.
А тут работает! Но это не прибавляет понимания проблемы... В чем же дело?
Из цппреференса про метод reserve:
То есть стандарт разрешает реализациям выделять больше элементов для мапы, чем мы запросили.
Легитимацию безобразия мы получили, но хотелось бы внятное объяснение причины предоставления такой возможности.
Реализации обычно выбирают bucket_count исходя из соображений быстродействия(как обычно). Тут они выбирают из двух опций:
1️⃣ Выбирают в качестве bucket_count степень двойки, то есть округляют до степени двойки в большую сторону. Это помогает эффективно маппить результат хэш функции на размер самой хэш-таблицы. Можно просто сделать битовое И и отбросить все биты, старше нашей степени. Что делается на один цикл цпу.
Но этот способ имеет негативный эффект в виде того же отбрасывания битов. То есть эти страшие биты никак не влияют на маппинг хэша на бакеты, то уменьшает равномерность распределения.
Таким способом пользуется Visual C++.
2️⃣ Поддерживают bucket_count простым числом.
Это дает крутой эффект того, что старшие биты также влияют на распределение объектов по бакетам. В этом случае даже плохие хэш-функции имеют более равномерное размещение бакетов.
Однако наивная реализация такого подхода заставляет каждый раз делить на рантаймовое значение bucket_count, что может занимать до 100 раз больше циклов.
Более быстрой альтернативой может быть использование захардкоженой таблицы простых чисел. Индекс в ней выбирается на основе запрашиваемого значения bucket_count. Таким образом компилятор может заоптимизировать деление по модулю через битовые операции, сложения, вычитания и умножения. Можете посмотреть на эти оптимизации более подробно на этом примере в годболт.
Этой реализацией пользуется GCC и Clang.
Вот такие страсти происходят у нас под носом под капотом неупорядоченной мапы.
Optimize everything. Stay cool.
#STL #optimization #compiler
#опытным
Стандартная ситуация. Создаем контейнер, резервируем подходящий размер для ожидаемого количества элементов в коллекции и запихиваем элементы. Все просто. Но это с каким-нибудь вектором все просто. А хэш-мапа - дело нетривиальное. Смотрим на код:
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
🔥33👍11❤2🤔2
Ревью
#опытным
На просторах интернета нашел интересный сэмпл кода, которым захотел с вами поделиться. Интересный, потому что он очень маленький, но там есть большое пространство для тщательного #ревью.
А еще у новичков могут глаза вытечь, посмотрев на первую строчку. В общем, равнодушным никто не останется.
Оставляйте в комментах свои замечания и возможные варианты исправления ситуации.
Комментарии наиболее продуктивного критика выложу на канал вместе с ответом.
Хватит отдыхать, пора ошибки в коде искать!
Analyse your life. Stay cool.
#опытным
На просторах интернета нашел интересный сэмпл кода, которым захотел с вами поделиться. Интересный, потому что он очень маленький, но там есть большое пространство для тщательного #ревью.
А еще у новичков могут глаза вытечь, посмотрев на первую строчку. В общем, равнодушным никто не останется.
Оставляйте в комментах свои замечания и возможные варианты исправления ситуации.
Комментарии наиболее продуктивного критика выложу на канал вместе с ответом.
Хватит отдыхать, пора ошибки в коде искать!
Analyse your life. Stay cool.
🔥18👍6❤4🤯3😭2🤨1
Результаты ревью
Круто вчера постарались, столько проблем нашли в этом маленьком кусочке кода. Больше всего проблем нашли два подписчика со скрина: я не смог выбрать из них одного, так как их пункты хоть и пересекаются, но все же дополняют друг друга различными мыслями. Давайте похлопаем нашим героям!👏👏👏👏👏👏
А теперь скомпануем все воедино. Напомню, что код был такой:
Сначала очевидное и то, что бросается в глаза. Все правильно поняли, что смысл кода - вывести элементы любого контейнера на выходной поток. Всвязи с этим следующие рассуждения:
❗️ Второй аргумент оператора принимается по значению, что ведет с излишним копированиям. Лучше использовать константную ссылку, так как мы не собираемся изменять значения контейнера.
❗️ В цикле тоже будут копирования, так как obj - объект, а не ссылка. Лучше использовать const auto &.
❗️ В первой строчке смешиваются class и typename. Это путает читателя, заставляя задумываться о тайном замысле использования разных ключевых слов. Лучше везде использовать class, так как Артем отметил , что до С++17 в шаблон-шаблонных параметрах нельзя было использовать typename.
❗️ Не очень выразительное название для аргумента, которым предполагается быть контейнеру. Хотя бы полностью написать Container.
❗️ Вывод элементов очень странный и кривой. Как минимум после последнего элемента будет ставиться пробел. Решить проблему можно с помощью стандартного алгоритма std::copy и интересного экспериментального итератора std::experimental::make_ostream_joiner, который может выводить элементы последовательности через разделитель, не записывая разделитель в конце! Выглядит это так:
На этом очевидные недостатки, которые мог выделить даже не очень разбирающийся в шаблонах читатель, заканчиваются.
Посмотрим чуть поглужбе. Функция используется для отладочных и учебных целей. Это понятно по использованию макроса 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
Круто вчера постарались, столько проблем нашли в этом маленьком кусочке кода. Больше всего проблем нашли два подписчика со скрина: я не смог выбрать из них одного, так как их пункты хоть и пересекаются, но все же дополняют друг друга различными мыслями. Давайте похлопаем нашим героям!👏👏👏👏👏👏
А теперь скомпануем все воедино. Напомню, что код был такой:
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
🔥23👏11❤3👍3
nullptr
#новичкам
Вероятно, каждый, кто писал код на C++03, имел удовольствие использовать NULL и постоянно ударяться мизинцем ноги об этот острый уголок тумбочки. Дело в том, что NULL использовался, как обозначение нулевого указателя, который никуда не указывает. Но если он для этого и использовался - это не значит, что он таковым являлся. Да и являлся он котом в мешке. Это макрос, который мог быть определен как
Вот в этом-то и вся проблема. NULL очень явно хочет себя видеть в роли указателя, но по факту в зеркале видит число. Допустим, у нас есть 2 перегрузки одной функции: одна для инта, вторая для указателя:
Намерения ясны: мы хотим вызвать перегрузку для указателя. Но это гарантировано не произойдет! В произойдет один из двух сценариев: если NULL определен как
Проблему можно решить енамами, принимать вместо его вместо инта и передавать для нулевого spellID что-то типа NoSpell. Но надо опять городить огород. Почему все не работает из коробки?!
С приходом С++11 начало работать из коробки. Надо только забыть про NULL и использовать nullptr.
Ключевое слово nullptr обозначает литерал указателя. Это prvalue типа std::nullptr_t. И nullptr неявно приводится к нулевому значению указателя для любого типа указателя. Это объект отдельного типа, который теперь к простому инту не приводится.
Поэтому сейчас этот код отработает как надо:
Так как nullptr - значение конкретного типа std::nullptr_t, то мы может принимать в функции непосредственно этот тип, а не общий тип указателя. Такая штука используется, например, в реализации std::function, конструктор которого имеет перегрузку для std::nullptr_t и делает тоже самое, что и конструктор без аргументов.
По той же причине nullptr даже при возврате через функцию может быть приведен к типу указателя. А вот обычные null pointer константны не могут похвастаться таким свойством. Они могут приводиться к указателям только в виде литералов.
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
#новичкам
Вероятно, каждый, кто писал код на 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
❤33👍15🔥5🤔2
Всем привет)
Мы для вас подготовили серию статей по выводу шаблонных параметров. Когда-то давно нас попросили рассказать про конструкцию decltype(auto), но про это сложно будет рассказывать, не разобрав по отдельности decltype и auto. Но в первую очередь нужно поговорить про вывод типов с плюсах, поэтому начнем с этого.
Здесь будет приведен список будущих тем. По мере появления постов здесь будут появляться гиперссылки для более удобного поиска. Также и сами темы могут добавляться по мере выхода статей.
Темы:
👉🏿 Вывод типов
👉🏿 Template type deduction
👉🏿 Небольшой пролог для вывода типов
👉🏿 ParamType - не cv-квалифицированная ссылка
👉🏿 ParamType - не cv-квалифицированный указатель
👉🏿 ParamType - cv-квалифицированный параметр
👉🏿 ParamType - универсальная ссылка
👉🏿 ParamType - не ссылка и не указатель
👉🏿 Аргумент шаблонной функции - массив
👉🏿 Аргумент шаблонной функции - функция
Возможно где-то вы уже это видели(привет, Скотт!). Однако оттуда мы взяли структуру, наполнение будет переформатировно и расширено.
Мы для вас подготовили серию статей по выводу шаблонных параметров. Когда-то давно нас попросили рассказать про конструкцию decltype(auto), но про это сложно будет рассказывать, не разобрав по отдельности decltype и auto. Но в первую очередь нужно поговорить про вывод типов с плюсах, поэтому начнем с этого.
Здесь будет приведен список будущих тем. По мере появления постов здесь будут появляться гиперссылки для более удобного поиска. Также и сами темы могут добавляться по мере выхода статей.
Темы:
👉🏿 Вывод типов
👉🏿 Template type deduction
👉🏿 Небольшой пролог для вывода типов
👉🏿 ParamType - не cv-квалифицированная ссылка
👉🏿 ParamType - не cv-квалифицированный указатель
👉🏿 ParamType - cv-квалифицированный параметр
👉🏿 ParamType - универсальная ссылка
👉🏿 ParamType - не ссылка и не указатель
👉🏿 Аргумент шаблонной функции - массив
👉🏿 Аргумент шаблонной функции - функция
Возможно где-то вы уже это видели(привет, Скотт!). Однако оттуда мы взяли структуру, наполнение будет переформатировно и расширено.
❤20👍8🔥5😢2
Вывод типов
#новичкам
С++ - статически типизированный язык, что значит, что типы всех объектов должны быть известны на этапе компиляции. Это хорошо для безопасности программы и предсказуемости поведения, но не очень хорошо с точки зрения удобства написания программы. Не всегда мне хочется писать что-то типа "im::so::tired::of::typing::long<types>::iterator". Точнее никогда.
Да, есть алиасы и синонимы, это нужные и полезные вещи. Но не на все же гигадлинные типы их вводить.
Очень хочется, чтобы работу по "написанию" типов делал кто-то за нас. Ведь в конце концов, все сигнатуры функций и методов известны, нормальные пацаны используют явные плюсовые касты, инициализаторы обычно представляют из себя понятные типы. Да, есть всякие приколы с неявным приведением типов и неэксплисит конструкторами от одного аргумента. Но попробовать-то стоит?
Так и подумали создатели С++11 и решили ввести для решения этой проблемы ключевое слово auto. На самом деле они ничего не вводили, а вдохнули новую жизнь в уже существующее ключевое слово. У нас даже пост про это есть.
Вещь - суперполезная и нужная. Сохраняет много пальчиковых усилий ленивым разработчикам. Например, у меня есть набор коллекций данных, каждая из которых связана с определенным идентификатором. Этот набор можно описать довольно просто:
Так вот, чтобы по этой мапе проитерироваться раньше нужно было писать вот так:
Это конечно никуда не годится, выглядит ужасно, нечитаемо, да и код повторяется. Теперь подключаем 11-у плюсы и случается магия:
А добавив заклинание под называнием range-based-for, получим:
Не идеально, это вам не питон. Но уже ощутимо приятнее и короче раза в 3.
Но тут встает вопрос: а как вообще эти типы-то выводятся? Есть наверное какие-то правила, алгоритм, по которому компилятор выводит тип?
Есть. Иначе это было бы магией(хотя грустновато без нее в нашем мире).
Его можно запомнить довольно легко. Поэтому в нескольких следующих постах мы будем разбирать эту тему.
А вообще знаете, что существует 3 вида вывода типов? Может и больше, но 3 точно есть, обещаю)
Delegate your work. Stay cool.
#cpp11
#новичкам
С++ - статически типизированный язык, что значит, что типы всех объектов должны быть известны на этапе компиляции. Это хорошо для безопасности программы и предсказуемости поведения, но не очень хорошо с точки зрения удобства написания программы. Не всегда мне хочется писать что-то типа "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
🔥31👍19❤5⚡1
Опасности std::unordered_map
#опытным
Когда писал прошлый пост, я хотел сразу вставить в пример range-based-for, чтобы показать одну приколюху. Но решил, что это заслуживает отдельного поста.
В копилку полезности auto.
Вдруг вы решили не пользоваться этой фичей и пишите вот так:
Вроде бы все хорошо и выглядит, как надо. И ожидать мы в консоли будем такой вывод:
При заполнении вектора кастомеры копируются из временных объектов, вызывается копирующий конструктор с принтом, и далее вывод цикла.
Однако на самом деле вывод будет такой:
Мы этого совсем не ожидали. Откуда еще 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.
Теперь у нас есть ожидаемый вывод.
Make your life easier. Stay cool.
#cpp11 #STL
#опытным
Когда писал прошлый пост, я хотел сразу вставить в пример 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
🔥29👍16❤5
Template type deduction
#новичкам
Пользователи 98-го стандарта недоумевали, почему они обязаны при наличии инициализатора указывать полный тип переменной при ее определении. "Если я еще раз напишу полный тип итератора, то я устрою Роскомнадзор", "Вы что, хотите, чтобы я пальцы стёр?!" и тд. У многих были такие мысли. И это, вообще говоря, было очень странно, потому что компилятор уже на тот момент мог сам выводить тип на основе типа другого выражения!
В шаблонах.
Вам же не обязательно писать в шаблонных функциях тип шаблонного аргумента. В некоторых случаях компилятор за вас это может сделать.
Здесь выведется 10 и, как вы видите, для функции my_size мы не указывали явным образом шаблонный тип.
Ну и раз уже есть наработанная схема, к которой разработчики уже привыкли, то почему бы именно ее не использовать в качестве основы вывода типов для auto? Этим риторическим вопросом задались контрибьютеры в 11-й стандарт и теперь у нас действительно есть ключевое слово auto, для которого вывод типов практически ничем не отличается от вывода типов для шаблонных функций!
Поэтому надо понимать, что это за зверь такой, чтобы осознанно использовать auto.
Чтобы мы все понимали, о чем конкретно будем говорить, посмотрим на следующий псевдокод:
Все будем разбирать на примере подобной шаблонной функции. Так вот процесс вывода типов ParamType и Т на основании типа выражения expression - это и есть вывод шаблонных типов.
Небольшой пример:
В случае 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
#новичкам
Пользователи 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
👍21🔥7❤5
Небольшой пролог для вывода типов
#новичкам
Посты дальше будут довольно хардкорные, но будет очень много примеров, так что готовьтесь вовлекаться в контекст и препарировать каждый пример, чтобы прям понять, как это все работает.
А сейчас я объясню некую концепцию, которая у меня сложилась в голове по ходу изучения нюансов вывод типов. Когда-то мне она помогла выровнять и законнектить все знания по этой теме, потому что примеров в интернете мало, иногда вкинут пару фраз про определенный кейс и все. А ты сиди и голову ломай, как этот черный ящик работает.
Статей с таким количеством примеров, как в следующих постах, исчезающе мало, поэтому контент уникальный. Надеюсь, вам понравится)
Коротко напомню контекст. ParamType - тип выражения-параметра функции. T - шаблонный тип функции :
Введу такую концепцию, как слои вложенности типов. Слои есть не у всех типов, а только у тех, у кого есть вложенность)
Вложенность актуальна в основном для указателей и шаблонных типов. Тип Т вложен в указатель(указетель - отдельный тип). Тип Т вложен в шаблонный контейнер(например).
Слои могут быть у типа expression и у типа param. Так вот одна из задач вывода - правильно заматчить слои этих типов друг на друга.
Представьте, что тип expression - это капуста и листы этой капусты - слои вложенности. Чтобы из типа expression грубо получить тип Т, нужно оторвать от капусты столько слоев, сколько есть в типе ParamType. И оставшаяся качерышка - и есть выведенный тип Т. Приведу примеры.
Простой одинокий шаблонный параметр.
Здесь нулевая вложенность типа параметра(нет слоев). Какую бы кракозябру вы бы туда не засунули, тип Т будет отличаться от типа expression разве что константностью и ссылочность. От капусты ни одного листа не отрываем и в выводе типа будут участвовать все слои expression.
Засунем туда переменную типа RandomType без вложенности - в выводе T будет полностью участвовать этот тип и по итогу Т будет равен RandomType.
Если засунем шаблонный тип std::set<int> с двумя слоями вложенности: внешним для std::set и внутренним для int, то в выводе будут участвовать оба слоя и Т будет иметь такой же тип std::set<int>. Снова ни одна капуста не пострадала.
Дальше ссылка
Казалось бы ссылка - это уже индирекция(под капотом лежит указатель). Однако с помощью ссылки вы только непосредственно объектом можете управлять! Поэтому в этом смысле никакой индирекции нет и тут также нулевая вложенность и рассуждения, как для предыдущего примера.
Указатель или вектор - уже появляется вложенность: наружный тип(указатель или шаблонный вектор) и внутренний тип. Так и получается, что у нас есть внутренний и внешний слой. И за счет того, что мы определили внешний слой(сказали, что наш параметр - указатель/вектор), в выводе параметра Т участвует только внутренний слой типа expression и все что в него вложено. Передам в func указатель на инт - от этой капусты отрываем внешний листок и остается тип инт, в который и выводится Т.
Если передам двойной указатель на инт int **, то мы убираем внешний слой указателя и от типа expression остается уже одинарный указатель на int *. И соответственно Т выведется в int *.
В чем проблема и зачем это все вводить и объяснять - большинство примеров в интернете ограничиваются лишь интами и ссылками на инты. Я же хочу разобрать чуть более сложные кейсы, которые не укладываются в стандартные объяснения происходящего.
Типы шаблонных параметров приблизительно так и выводятся, как описано в этом посте. Однако есть нюансы с константностью и ссылочностью, которые придется разобрать в следующих постах.
Поддержите пост лайками, если хотите подробного разбора этой темы.
Support hardcore stuff. Stay cool.
#cppcore #template
#новичкам
Посты дальше будут довольно хардкорные, но будет очень много примеров, так что готовьтесь вовлекаться в контекст и препарировать каждый пример, чтобы прям понять, как это все работает.
А сейчас я объясню некую концепцию, которая у меня сложилась в голове по ходу изучения нюансов вывод типов. Когда-то мне она помогла выровнять и законнектить все знания по этой теме, потому что примеров в интернете мало, иногда вкинут пару фраз про определенный кейс и все. А ты сиди и голову ломай, как этот черный ящик работает.
Статей с таким количеством примеров, как в следующих постах, исчезающе мало, поэтому контент уникальный. Надеюсь, вам понравится)
Коротко напомню контекст. ParamType - тип выражения-параметра функции. T - шаблонный тип функции :
template <class T>
void func(ParamType param) {...}
func(expression);
Введу такую концепцию, как слои вложенности типов. Слои есть не у всех типов, а только у тех, у кого есть вложенность)
Вложенность актуальна в основном для указателей и шаблонных типов. Тип Т вложен в указатель(указетель - отдельный тип). Тип Т вложен в шаблонный контейнер(например).
Слои могут быть у типа expression и у типа param. Так вот одна из задач вывода - правильно заматчить слои этих типов друг на друга.
Представьте, что тип expression - это капуста и листы этой капусты - слои вложенности. Чтобы из типа expression грубо получить тип Т, нужно оторвать от капусты столько слоев, сколько есть в типе ParamType. И оставшаяся качерышка - и есть выведенный тип Т. Приведу примеры.
Простой одинокий шаблонный параметр.
template <class T>
void func(T param) {...}
Здесь нулевая вложенность типа параметра(нет слоев). Какую бы кракозябру вы бы туда не засунули, тип Т будет отличаться от типа expression разве что константностью и ссылочность. От капусты ни одного листа не отрываем и в выводе типа будут участвовать все слои expression.
Засунем туда переменную типа RandomType без вложенности - в выводе T будет полностью участвовать этот тип и по итогу Т будет равен RandomType.
Если засунем шаблонный тип std::set<int> с двумя слоями вложенности: внешним для std::set и внутренним для int, то в выводе будут участвовать оба слоя и Т будет иметь такой же тип std::set<int>. Снова ни одна капуста не пострадала.
Дальше ссылка
template <class T>
void func(T& param) {...}
Казалось бы ссылка - это уже индирекция(под капотом лежит указатель). Однако с помощью ссылки вы только непосредственно объектом можете управлять! Поэтому в этом смысле никакой индирекции нет и тут также нулевая вложенность и рассуждения, как для предыдущего примера.
template <class T>
void func(T * param) {...}
template <class T>
void func1(std::vector<T> param) {...}
Указатель или вектор - уже появляется вложенность: наружный тип(указатель или шаблонный вектор) и внутренний тип. Так и получается, что у нас есть внутренний и внешний слой. И за счет того, что мы определили внешний слой(сказали, что наш параметр - указатель/вектор), в выводе параметра Т участвует только внутренний слой типа expression и все что в него вложено. Передам в func указатель на инт - от этой капусты отрываем внешний листок и остается тип инт, в который и выводится Т.
Если передам двойной указатель на инт int **, то мы убираем внешний слой указателя и от типа expression остается уже одинарный указатель на int *. И соответственно Т выведется в int *.
В чем проблема и зачем это все вводить и объяснять - большинство примеров в интернете ограничиваются лишь интами и ссылками на инты. Я же хочу разобрать чуть более сложные кейсы, которые не укладываются в стандартные объяснения происходящего.
Типы шаблонных параметров приблизительно так и выводятся, как описано в этом посте. Однако есть нюансы с константностью и ссылочностью, которые придется разобрать в следующих постах.
Поддержите пост лайками, если хотите подробного разбора этой темы.
Support hardcore stuff. Stay cool.
#cppcore #template
👍43❤26🔥8😁2
Как посмотреть шаблонный тип
#новичкам
Вчера Антон сделал важное замечание, что неплохо бы показать, как самому посмотреть, во что выводится тип Т в каждом конкретном случае. Собсна, погнали.
В С++ стандартными средствами конечно можно это сделать, но решение будет довольно громоздкое и некрасивое с точки зрения пользователя.
Хотелось бы что-то очень простое, желательно вообще однострочное. Обычно таких решений в плюсах нет и надо городить огород, но не в этом случае. Благодаря обширным возможностям препроцессора компиляторы зачастую определяют свои макросы, которые раскрываются в сигнатуру функции. В случае же с шаблонной функцией, они показывают и правильный выведенный шаблонный тип.
Для шланга и гцц этот макрос называется __PRETTY_FUNCTION__, а для msvc - __FUNCSIG__. Пользоваться ими можно примерно так:
Для кланга вывод будет такой:
Для msvc:
Тут на мой взгляд msvc предоставляет несколько более полный и понятный функционал, но кому как удобно.
Можете поиграться в годболте.
See through things. Stay cool.
#compiler #template
#новичкам
Вчера Антон сделал важное замечание, что неплохо бы показать, как самому посмотреть, во что выводится тип Т в каждом конкретном случае. Собсна, погнали.
В С++ стандартными средствами конечно можно это сделать, но решение будет довольно громоздкое и некрасивое с точки зрения пользователя.
Хотелось бы что-то очень простое, желательно вообще однострочное. Обычно таких решений в плюсах нет и надо городить огород, но не в этом случае. Благодаря обширным возможностям препроцессора компиляторы зачастую определяют свои макросы, которые раскрываются в сигнатуру функции. В случае же с шаблонной функцией, они показывают и правильный выведенный шаблонный тип.
Для шланга и гцц этот макрос называется __PRETTY_FUNCTION__, а для msvc - __FUNCSIG__. Пользоваться ими можно примерно так:
#if defined __clang__ || __GNUC__
#define FUNCTION_SIGNATURE __PRETTY_FUNCTION__
#elif defined __FUNCSIG__
#define FUNCTION_SIGNATURE __FUNCSIG__
#endif
template<class T>
void func(const T& param) {
std::cout << FUNCTION_SIGNATURE << std::endl;
}
func(std::vector<int>{});
Для кланга вывод будет такой:
void func(const T &) [T = std::vector<int>]
Для msvc:
void __cdecl func<class std::vector<int,class std::allocator<int> >>(const class std::vector<int,class std::allocator<int> > &)
Тут на мой взгляд msvc предоставляет несколько более полный и понятный функционал, но кому как удобно.
Можете поиграться в годболте.
See through things. Stay cool.
#compiler #template
❤19👍11🔥3
Квиз
Сегодня будет довольно противоречивый #quiz. Ничего не буду говорить. Просто задам вопрос. А подробный ответ будет вечером.
Какой результат попытки компиляции и выполнения этого кода:
Сегодня будет довольно противоречивый #quiz. Ничего не буду говорить. Просто задам вопрос. А подробный ответ будет вечером.
Какой результат попытки компиляции и выполнения этого кода:
#include <algorithm>
#include <iostream>
struct foo {
static const int qwerty = 100;
};
int main() {
std::cout << std::max(0, foo::qwerty) << std::endl;
return 0;
}
🔥12❤3👍3👎2
Результат?
Anonymous Poll
29%
Ошибка компиляции
16%
Ошибка линковки
4%
Segmentation fault
4%
0
44%
100
2%
100500
❤7👍3🔥2
Ответ
Несмотря на всю простоту и краткость кода, он не запустится! Будет ошибка линковки undefined reference to foo::qwerty.
Как такое возможно, если мы четко определили статическое поле qwerty?
Обратимся к стандарту:
Мы хоть и можем указывать инициализатор для константного статического интегрального поля, но это скорее что-то типа маркера инициализации. То есть именно с этим инициализатором будет определяться поле.
И этот маркет не является определением! Оно все равно нужно, так как foo::qwerty odr-использована. Один из признаков того, что переменная odr-используется - на нее ссылается ссылка.
Так вот функция std::max принимает константные ссылки на объекты. А передаем мы в нее lvalue. Значит у этого lvalue должен быть существующий адрес, чтобы нормально забиндится на ссылку.
Вот и получается порочная цепочка: чтобы забиндится на ссылку нам нужен адрес -> адрес глобальная переменная получает только если у нее есть определение -> так как мы используем ссылку на статическое поле класса нам нужно его определение -> мы его не предоставили -> получили по рукам от линкера.
Чтобы избежать ошибки можно использовать 3 стратегии:
👉🏿 Честно предоставить определение, согласно стандарту, без инициализатора.
👉🏿 Использовать ключевое слово inline, которое, начиная с С++17, позволяет определять поля внутри описания класса.
👉🏿 Вместо const использовать ключевое слово constexpr. Это фактически сразу делает qwerty компайл тайм константой, которая должна быть инициализирована при объявлении и к которой спокойно можно обращаться в том числе и по ссылке.
Вот такое тонкое место есть в С++)
Surprise everyone. Stay cool.
Несмотря на всю простоту и краткость кода, он не запустится! Будет ошибка линковки undefined reference to foo::qwerty.
Как такое возможно, если мы четко определили статическое поле qwerty?
Обратимся к стандарту:
If a non-volatile non-inline const static data member is of integral
or enumeration type, its declaration in the class definition can specify
a brace-or-equal-initializer in which every initializer-clause that is an
assignment-expression is a constant expression.
The member shall still be defined in a namespace scope if it is odr-used
in the program and the namespace scope definition shall not contain an initializer
Мы хоть и можем указывать инициализатор для константного статического интегрального поля, но это скорее что-то типа маркера инициализации. То есть именно с этим инициализатором будет определяться поле.
И этот маркет не является определением! Оно все равно нужно, так как foo::qwerty odr-использована. Один из признаков того, что переменная odr-используется - на нее ссылается ссылка.
Так вот функция std::max принимает константные ссылки на объекты. А передаем мы в нее lvalue. Значит у этого lvalue должен быть существующий адрес, чтобы нормально забиндится на ссылку.
Вот и получается порочная цепочка: чтобы забиндится на ссылку нам нужен адрес -> адрес глобальная переменная получает только если у нее есть определение -> так как мы используем ссылку на статическое поле класса нам нужно его определение -> мы его не предоставили -> получили по рукам от линкера.
Чтобы избежать ошибки можно использовать 3 стратегии:
👉🏿 Честно предоставить определение, согласно стандарту, без инициализатора.
struct foo {
static const int qwerty = 100;
};
const int foo::qwerty;👉🏿 Использовать ключевое слово inline, которое, начиная с С++17, позволяет определять поля внутри описания класса.
struct foo {
inline static const int qwerty = 100;
};👉🏿 Вместо const использовать ключевое слово constexpr. Это фактически сразу делает qwerty компайл тайм константой, которая должна быть инициализирована при объявлении и к которой спокойно можно обращаться в том числе и по ссылке.
struct foo {
static constexpr int qwerty = 100;
};Вот такое тонкое место есть в С++)
Surprise everyone. Stay cool.
👍32❤🔥11⚡6🔥6❤1
Один плюс решает все
Вчера мы рассматривали такой код и он фейлился при линковке:
Но стоит нам добавить всего лишь + к имени переменной foo::qwerty и код сразу же начнет компилироваться и выдавать ожидаемый результат.
Почему?
Для интов определено унарный оперетор +, который возвращает временное значение. Он не реализован в рамках обычных функций С++ и компилятор может как угодно его оптимизировать, но главное, что нам нужно знать - компилятор рассматривает это как новое rvalue значение. Которое может кастится к константной ссылке и эта операция не требует наличия определенного адреса объекта. А так как оригинальная переменная foo::qwerty теперь не odr-used(от нее больше не берут ссылку), то и компилятору не нужно больше определение. Он прекрасно видит значение инициализатора и может просто подставить на место foo::qwerty значение его инициализатора.
Прикол в том, что оптимизации компилятора могут помочь вам угомонить линкер. Даже вчерашний пример прекрасно собирается с оптимизациями. Однако это вообще не значит, что в программе все хорошо. Вы по прежнему должны определить это статическое поле, чтобы использовать переменную согласно стандарту, чтобы ее можно было корректно использовать во всех сценариях.
Focus on positive. Stay cool.
#cppcore
Вчера мы рассматривали такой код и он фейлился при линковке:
#include <algorithm>
struct foo {
static const int qwerty = 100;
};
int main() {
std::cout << std::max(0, foo::qwerty) << std::endl;
return 0;
}
Но стоит нам добавить всего лишь + к имени переменной foo::qwerty и код сразу же начнет компилироваться и выдавать ожидаемый результат.
int main() {
std::cout << std::max(0, +foo::qwerty) << std::endl;
return 0;
}Почему?
Для интов определено унарный оперетор +, который возвращает временное значение. Он не реализован в рамках обычных функций С++ и компилятор может как угодно его оптимизировать, но главное, что нам нужно знать - компилятор рассматривает это как новое rvalue значение. Которое может кастится к константной ссылке и эта операция не требует наличия определенного адреса объекта. А так как оригинальная переменная foo::qwerty теперь не odr-used(от нее больше не берут ссылку), то и компилятору не нужно больше определение. Он прекрасно видит значение инициализатора и может просто подставить на место foo::qwerty значение его инициализатора.
Прикол в том, что оптимизации компилятора могут помочь вам угомонить линкер. Даже вчерашний пример прекрасно собирается с оптимизациями. Однако это вообще не значит, что в программе все хорошо. Вы по прежнему должны определить это статическое поле, чтобы использовать переменную согласно стандарту, чтобы ее можно было корректно использовать во всех сценариях.
Focus on positive. Stay cool.
#cppcore
🔥33👍8❤6
ParamType - не cv-квалифицированная ссылка
#новичкам
Список постов по теме , Пост про слои
Пойдем по порядку мажорных вариантов. Первым в очереди случай, когда ParamType - не cv-квалифицированная(без пометок const и volatile), не универсальная ссылка. Дальше только про константность буду говорить в контексте cv-квалификаторов, а на volatile забью(впрочем, ему не привыкать).
Первый вариант может быть такой:
Тут порядок такой: берете полный тип expression -> откидывайте от него внешнюю ссылочность, если она есть -> все, что осталось, запихиваете в Т. Так как ParamType не имеет слоев вложенности, то мы ничего не отрываем от типа expression. Таким образом параметр Т никак не может быть ссылочным типом, а тип ParamType - всегда ссылка. Пример:
Пойдем по порядку. С переменной
Теперь добавим щепотку константности. У
Для const_ref_x сначала откидываем ссылочность и все оставшееся пихаем в Т, который выведется в const int.
Давайте очень важную особенность проследим. Каждый раз, когда мы объявляем константу или константную ссылку и передаем их в шаблон, ParamType которого T&, тип Т оказывается тоже константой. Это очень важный момент для обобщенного программирования: в функцию кто-то может передать константу. И он очень естественно ожидает, что значение его переменной не изменится. Ну может и не ожидает(в плюсах нужно настроиться ожидать что угодно), но очень хочет, чтобы оно не менялось. Иначе БУМ! И вот такой механизм сохранения константности шаблонного типа и позволяет шаблонным функциям, принимающим неконстантную ссылку вида Т&, спокойно принимать в себя константные объекты и не изменять их(так как сам тип неизменяемый).
А что если ParamType будет вложенным типом?
В этом случае мы явно сказали, что хотим принимать какой-то вектор. Мы просто отрываем этот слой вместе со ссылочностью и оставшееся - наш тип Т.
Но! Так как тип внешнего слоя полностью специфицирован, то у нас никак не получится передать в функцию константный вектор или ссылку на константный вектор. То есть тут сама сигнатура функции подразумевает изменяемый внешний слой, а мы пытаемся туда передать неизменяемый объект. Компилятор запрещает нам такие мувы делать.
Почему в одном случае можно передать константую ссылку, а в другом нет? Все из-за волшебного типа Т, который может быть кем угодно. Ссылка Т& может быть ссылкой на любой тип, в том числе и константный. Можете прям так и читать: ссылка на что угодно. Этот Т как бы вбирает в себя все особенности типа.
А для такого выражения std::vector<T>& мы читаем: ссылка на вектор от чего-угодно. Внешний слой зафиксирован, а внутренний может содержать в себе что-угодно.
Protect your invariants. Stay cool.
#cppcore #template
#новичкам
Список постов по теме , Пост про слои
Пойдем по порядку мажорных вариантов. Первым в очереди случай, когда ParamType - не cv-квалифицированная(без пометок const и volatile), не универсальная ссылка. Дальше только про константность буду говорить в контексте cv-квалификаторов, а на volatile забью(впрочем, ему не привыкать).
Первый вариант может быть такой:
template <class T>
void func(T& param) {...}
// | |
// ParamType
func(expression);
// decltype(expression) - expression's type
Тут порядок такой: берете полный тип expression -> откидывайте от него внешнюю ссылочность, если она есть -> все, что осталось, запихиваете в Т. Так как ParamType не имеет слоев вложенности, то мы ничего не отрываем от типа expression. Таким образом параметр Т никак не может быть ссылочным типом, а тип ParamType - всегда ссылка. Пример:
template <class T>
void func(T& param) {...}
int x = 42; // x is an int
const int const_x = x; // const_x is a const int
const int& const_ref_x = x; // const_ref_x is a reference to x as a const int
std::list<double> lst;
func(x); // T is int, ParamType is int&
func(const_x); // T is const int, ParamTypeis const int&
func(const_ref_x); // T is const int, ParamType is const int&
func(lst); // T is std::list<double>, ParamType is std::list<double>&
Пойдем по порядку. С переменной
x все сильно очевидно: тип param - int&, тип Т - int. Как и с lst: тип param - std::list<double>&, тип Т - std::list<double>Теперь добавим щепотку константности. У
const_x нет ссылочности, поэтому запихивает полный ее тип в Т, который выведется в const int.Для const_ref_x сначала откидываем ссылочность и все оставшееся пихаем в Т, который выведется в const int.
Давайте очень важную особенность проследим. Каждый раз, когда мы объявляем константу или константную ссылку и передаем их в шаблон, ParamType которого T&, тип Т оказывается тоже константой. Это очень важный момент для обобщенного программирования: в функцию кто-то может передать константу. И он очень естественно ожидает, что значение его переменной не изменится. Ну может и не ожидает(в плюсах нужно настроиться ожидать что угодно), но очень хочет, чтобы оно не менялось. Иначе БУМ! И вот такой механизм сохранения константности шаблонного типа и позволяет шаблонным функциям, принимающим неконстантную ссылку вида Т&, спокойно принимать в себя константные объекты и не изменять их(так как сам тип неизменяемый).
А что если ParamType будет вложенным типом?
template <class T>
void func(std::vector<T>& param) {...}
// | |
// ParamType
std::vector<int> vec(10, 0);
const std::vector<int> const_vec(10, 0);
std::vector<int>& ref_vec = vec;
int a = 0;
int b = 1;
std::vector<std::reference_wrapper<int>> vec_of_ref{a, b};
func(vec); // T is int, ParamType is std::vector<int>&
func(const_vec); // ERROR!
func(ref_vec); // T is int, ParamType is std::vector<int>&
func(vec_of_ref); // T is std::reference_wrapper<int>, ParamType is std::vector<std::reference_wrapper<int>>&
В этом случае мы явно сказали, что хотим принимать какой-то вектор. Мы просто отрываем этот слой вместе со ссылочностью и оставшееся - наш тип Т.
Но! Так как тип внешнего слоя полностью специфицирован, то у нас никак не получится передать в функцию константный вектор или ссылку на константный вектор. То есть тут сама сигнатура функции подразумевает изменяемый внешний слой, а мы пытаемся туда передать неизменяемый объект. Компилятор запрещает нам такие мувы делать.
Почему в одном случае можно передать константую ссылку, а в другом нет? Все из-за волшебного типа Т, который может быть кем угодно. Ссылка Т& может быть ссылкой на любой тип, в том числе и константный. Можете прям так и читать: ссылка на что угодно. Этот Т как бы вбирает в себя все особенности типа.
А для такого выражения std::vector<T>& мы читаем: ссылка на вектор от чего-угодно. Внешний слой зафиксирован, а внутренний может содержать в себе что-угодно.
Protect your invariants. Stay cool.
#cppcore #template
👍19🔥6❤2🤯2
ParamType - не cv-квалифицированный указатель
#новичкам
Список постов по теме , Пост про слои
Переходим к выводу параметров для указателей. Давайте просто накидаем много указателей разных типов и посмотрим, какой тип шаблонного параметра выведется для них.
В комментах к строчкам все расписано, что и во что выводится. Я же обращу внимание на принцип. Снова возвращаемся к капусте. ParamType - указатель, а значит от типа выражения, переданного в функцию, мы должны этот указатель и оторвать и получим тип Т. Так и происходит во всех случаях. Если передаем константный указатель - снимаем этот слой вместе с константностью.
Здесь все просто, работает также как и со ссылками. Почти. Семантика сохранения константности шаблонного типа повторяется. То есть если указатель указывает на константный инт, то тип Т тоже будет константным. Однако, если константной ссылки не может быть(то что в народе называют константной ссылкой - это на самом деле ссылка на константный объект: сама по себе ссылка неизменяема, она просто может указывать на другой объект), то указатель может быть константным. То есть здесь уже играют роль слои вложенности. В этом случае, константность внутреннего слоя(который ближе к самому объекту) непосредственно отражается на шаблонном параметре Т, а константность внешнего слоя к типу Т не будет иметь отношения. Примерами здесь являются
В этом примере у типа param аж 2 слоя вложенности определены: 1 на указатель и 2 на контейнер. От типа аргумента в начале отрезаем указатель вместе с константностью, а далее и слой с std::list. По итогу тип Т выводится в то, что стоит в треугольных скобках у листа.
Есть одна интересная деталь: сигнатура функции подразумевает, что сам указатель не будет константным, то есть его можно изменять. И если вы передадите в нее константный указатель, то эта константность очень неожиданно пропадает и расплывается в пучине правил вывода типов. Так происходит с переменными
Dig deeper. Stay cool.
#template #cppcore
#новичкам
Список постов по теме , Пост про слои
Переходим к выводу параметров для указателей. Давайте просто накидаем много указателей разных типов и посмотрим, какой тип шаблонного параметра выведется для них.
template <class T>
void func(T* param) {...}
// | |
// ParamType
int x = 42;
int * p_x = &x;
const int * p_const_x = &x; // p_const_x is a ptr to const int
int * const const_p_x = &x; // const_p_x is a const ptr to int
const int * const const_p_const_x = &x; // const_p_const_x is a const ptr to const int
int ** p_p_x = &p_x; // p_p_x is a ptr to a ptr to x as int
const int * const * const const_p_const_p_const_x = &const_p_const_x; // const_p_const_p_const_x is a const ptr to a const ptr to const int
func(p_x); // T is int, param's type is int*
func(p_const_x); // T is const int, param's type is const int*
func(const_p_x); // T is int, param's type is int *
func(const_p_const_x); // T is const int, param's type is const int *
func(p_p_x); // T is int *, param's type is int **
func(const_p_const_p_const_x); // T is const int * const, param's type is const int * const *
В комментах к строчкам все расписано, что и во что выводится. Я же обращу внимание на принцип. Снова возвращаемся к капусте. ParamType - указатель, а значит от типа выражения, переданного в функцию, мы должны этот указатель и оторвать и получим тип Т. Так и происходит во всех случаях. Если передаем константный указатель - снимаем этот слой вместе с константностью.
Здесь все просто, работает также как и со ссылками. Почти. Семантика сохранения константности шаблонного типа повторяется. То есть если указатель указывает на константный инт, то тип Т тоже будет константным. Однако, если константной ссылки не может быть(то что в народе называют константной ссылкой - это на самом деле ссылка на константный объект: сама по себе ссылка неизменяема, она просто может указывать на другой объект), то указатель может быть константным. То есть здесь уже играют роль слои вложенности. В этом случае, константность внутреннего слоя(который ближе к самому объекту) непосредственно отражается на шаблонном параметре Т, а константность внешнего слоя к типу Т не будет иметь отношения. Примерами здесь являются
const_p_x, const_p_const_x, const_p_const_p_const_x.template <class T>
void func(std::list<T> * param) {...}
// | |
// ParamType
std::list<double> lst;
std::list<std::unique_ptr<const double>> lst_of_const;
std::list<std::vector<std::unique_ptr<const int>>> lst_vec_of_const;
std::list<std::vector<std::unique_ptr<const int>>> * const const_p_lst_vec_of_const = &lst_vec_of_const;
func(&lst); // T is double, param's type is std::list<double>
func(&lst_of_const); // T is std::unique_ptr<const double>, param's type is std::list<std::unique_ptr<const double>>*
func(&lst_vec_of_const); // T is std::vector<std::unique_ptr<const int>>, param's type is std::list<std::vector<std::unique_ptr<const int>>>*
func(const_p_lst_vec_of_const); // T is std::vector<std::unique_ptr<const int>>, param's type is std::list<std::vector<std::unique_ptr<const int>>>*
В этом примере у типа param аж 2 слоя вложенности определены: 1 на указатель и 2 на контейнер. От типа аргумента в начале отрезаем указатель вместе с константностью, а далее и слой с std::list. По итогу тип Т выводится в то, что стоит в треугольных скобках у листа.
Есть одна интересная деталь: сигнатура функции подразумевает, что сам указатель не будет константным, то есть его можно изменять. И если вы передадите в нее константный указатель, то эта константность очень неожиданно пропадает и расплывается в пучине правил вывода типов. Так происходит с переменными
const_p_x, const_p_const_x, const_p_const_p_const_x и const_p_lst_vec_of_const Если для нешаблонной функции с параметром неконстантного указателя при передаче в нее константного указателя была бы ошибка компиляции, то здесь эта штука проходит фэйс-контроль. Помните об этой об этой особенности и потенциальной опасности.Dig deeper. Stay cool.
#template #cppcore
Telegram
Грокаем C++
Всем привет)
Мы для вас подготовили серию статей по выводу шаблонных параметров. Когда-то давно нас попросили рассказать про конструкцию decltype(auto), но про это сложно будет рассказывать, не разобрав по отдельности decltype и auto. Но в первую очередь…
Мы для вас подготовили серию статей по выводу шаблонных параметров. Когда-то давно нас попросили рассказать про конструкцию decltype(auto), но про это сложно будет рассказывать, не разобрав по отдельности decltype и auto. Но в первую очередь…
👍13🔥7❤3❤🔥2