Защищенные методы vs защищенные поля
ООП - вещь занятная и многогранная. Почему-то пришла в голову аналогия с математикой: есть начальные заданные правила(принципы и аксиомы), благодаря которым выводятся весьма нетривиальные следствия. Сегодня поговорим о довольно базовом следствии, которое однако далеко не у всех в голове есть.
Есть у вас класс и вы хотите дать его наследникам и только им возможность изменять поля класса. И у нас для этого есть два подхода: объявить поля protected и дать возможность наследникам изменять их напрямую или ввести protected методы, которые определяют полный набор изменений этих полей. Какой вариант более предпочтительный и почему?
Сразу раскрою карты: лучше определять защищенный интерфейс вместо прямого доступа к полям. Для этого есть несколько причин:
💥 Как только член класса становится более "доступен", чем private, вы даете гарантии другим классам о том, как этот член будет себя вести. Точнее никаких гарантий вы не даете. Поскольку поле совершенно неконтролируемо, размещение его "в дикой природе" открывает вашему классу и классам, которые наследуют от вашего класса или взаимодействуют с ним, прекрасный вид на саванну, а точнее море ошибок. Нет никакого способа узнать, когда меняется поле, нет никакого способа контролировать, кто или что его меняет.
💥 Если вы хотите хоть что-то похожее на безопасное приложения, вам нужны всякого рода проверки . Иногда они могут занимать несколько строчек кода. И если ваш код зависит от какого-то состояния класса, то при каждом использовании защищенного поля, вам нужно проверять, все ли с ним в порядке, все ли валидно. Насколько же проще вынести все такие проверки в защищенный метод и закрыть у себя в голове этот гештальт.
💥Когда вы определяете защищенный интерфейс, вы автоматически ограничиваете наследников в использовании ваших приватных членов, которые могли бы быть защищенными. А ограничения в программировании - это хорошо! Чем меньше вы позволяете сделать непотребств со своим классом или использовать его вне предполагаемых сценариях использования - тем лучше!
💥 Это даже тестирование упрощает. Есть четкие сценарии поведения, которые можно проверить на корректность. Очень легко с такими исходными данными писать тесты. А легкость тестирования мотивирует его в принципе делать, а не класть на него лысину(ставь лайк, если понял game of words).
💥 Количество наследников у класса может быть много и все будут завязаны на самостоятельном управлении protected членами. А если в будущем обработка этих полей изменится, то необходимы будут изменения во всех наследниках. Защищенный интерфейс делает все такие изменения локализованными в родительском классе, то есть в одном месте. Это снижает стоимость внесения изменений.
Инкапсуляция - гениальная вещь. Придерживаясь этого принципа, вы защищайте свои данные непреднамеренного изменения и позволяете изменениям не распространяться за пределы одного класса.
Protect your secrets. Stay cool.
#OOP #goodpractice #design
ООП - вещь занятная и многогранная. Почему-то пришла в голову аналогия с математикой: есть начальные заданные правила(принципы и аксиомы), благодаря которым выводятся весьма нетривиальные следствия. Сегодня поговорим о довольно базовом следствии, которое однако далеко не у всех в голове есть.
Есть у вас класс и вы хотите дать его наследникам и только им возможность изменять поля класса. И у нас для этого есть два подхода: объявить поля protected и дать возможность наследникам изменять их напрямую или ввести protected методы, которые определяют полный набор изменений этих полей. Какой вариант более предпочтительный и почему?
Сразу раскрою карты: лучше определять защищенный интерфейс вместо прямого доступа к полям. Для этого есть несколько причин:
💥 Как только член класса становится более "доступен", чем private, вы даете гарантии другим классам о том, как этот член будет себя вести. Точнее никаких гарантий вы не даете. Поскольку поле совершенно неконтролируемо, размещение его "в дикой природе" открывает вашему классу и классам, которые наследуют от вашего класса или взаимодействуют с ним, прекрасный вид на саванну, а точнее море ошибок. Нет никакого способа узнать, когда меняется поле, нет никакого способа контролировать, кто или что его меняет.
💥 Если вы хотите хоть что-то похожее на безопасное приложения, вам нужны всякого рода проверки . Иногда они могут занимать несколько строчек кода. И если ваш код зависит от какого-то состояния класса, то при каждом использовании защищенного поля, вам нужно проверять, все ли с ним в порядке, все ли валидно. Насколько же проще вынести все такие проверки в защищенный метод и закрыть у себя в голове этот гештальт.
💥Когда вы определяете защищенный интерфейс, вы автоматически ограничиваете наследников в использовании ваших приватных членов, которые могли бы быть защищенными. А ограничения в программировании - это хорошо! Чем меньше вы позволяете сделать непотребств со своим классом или использовать его вне предполагаемых сценариях использования - тем лучше!
💥 Это даже тестирование упрощает. Есть четкие сценарии поведения, которые можно проверить на корректность. Очень легко с такими исходными данными писать тесты. А легкость тестирования мотивирует его в принципе делать, а не класть на него лысину(ставь лайк, если понял game of words).
💥 Количество наследников у класса может быть много и все будут завязаны на самостоятельном управлении protected членами. А если в будущем обработка этих полей изменится, то необходимы будут изменения во всех наследниках. Защищенный интерфейс делает все такие изменения локализованными в родительском классе, то есть в одном месте. Это снижает стоимость внесения изменений.
Инкапсуляция - гениальная вещь. Придерживаясь этого принципа, вы защищайте свои данные непреднамеренного изменения и позволяете изменениям не распространяться за пределы одного класса.
Protect your secrets. Stay cool.
#OOP #goodpractice #design
👍13🔥7❤2
shared_ptr и массивы
Есть одна не самая приятная вещь при работе с std::shared_ptr. С момента его выхода в С++11 и в С++14 он не может быть использован из коробки для того, чтобы хранить динамические массивы. По дефолту во всех случаях при исчерпании ссылок на объект, шареный указатель вызывает оператор delete. Однако, когда мы аллоцируем динамический массив new[], мы хотим вызвать delete[] для его удаления. Но shared_ptr просто вызовет delete. А это неопределенное поведение.
То есть я не могу просто так вот взять и написать
Кстати говоря, у его собрата std::unique_ptr с этим все получше. У него есть отдельная частичная специализация для массивов. Поэтому вот так я могу написать спокойно:
Что можно сделать, чтобы таки использовать сишные массивы с шареным указателем?
👉🏿 Обернуть указатель на массив в класс и шарить уже объекты этого класса. Типа того(упрощенно):
У такого метода есть 2 проблемы. Первое - прокси класс. Дополнительные обертки увеличивают объем и сложность кода и затрудняют его понимание. Второе - перформанс. Здесь уже два уровня индирекции, что замедлит обработку.
👉🏿 Передать свой кастомный делитер. Тут тоже несколько вариантов.
⚡️Написать свой:
⚡️Использовать лямбду:
⚡️Ну или воспользоваться уже готовым вариантом:
std::default_delete имеет частичную специализацию для массивов.
Но! Какой хороший все-таки стандарт С++17, который поправил многие такие маленькие косячки. А как он это сделал - увидим в следующий раз)
Be comfortable to work with. Stay cool.
#cpp11 #memory
Есть одна не самая приятная вещь при работе с std::shared_ptr. С момента его выхода в С++11 и в С++14 он не может быть использован из коробки для того, чтобы хранить динамические массивы. По дефолту во всех случаях при исчерпании ссылок на объект, шареный указатель вызывает оператор delete. Однако, когда мы аллоцируем динамический массив new[], мы хотим вызвать delete[] для его удаления. Но shared_ptr просто вызовет delete. А это неопределенное поведение.
То есть я не могу просто так вот взять и написать
shared_ptr<int[]> sp(new int[10]);
Кстати говоря, у его собрата std::unique_ptr с этим все получше. У него есть отдельная частичная специализация для массивов. Поэтому вот так я могу написать спокойно:
std::unique_ptr<int[]> up(new int[10]); // вызовется корректный delete[]
Что можно сделать, чтобы таки использовать сишные массивы с шареным указателем?
👉🏿 Обернуть указатель на массив в класс и шарить уже объекты этого класса. Типа того(упрощенно):
template <class T>
struct DynamicArrayWrapper {
DynamicArrayWrapper(size_t size) : ptr{new T[size]} {}
~DynamicArrayWrapper() {delete[] ptr;}
T * ptr;
};
std::shared_ptr<DynamicArrayWrapper> sp{10};
У такого метода есть 2 проблемы. Первое - прокси класс. Дополнительные обертки увеличивают объем и сложность кода и затрудняют его понимание. Второе - перформанс. Здесь уже два уровня индирекции, что замедлит обработку.
👉🏿 Передать свой кастомный делитер. Тут тоже несколько вариантов.
⚡️Написать свой:
template< typename T >
struct array_deleter
{
void operator ()( T const * p)
{
delete[] p;
}
};
std::shared_ptr<int> sp(new int[10], array_deleter<int>());
⚡️Использовать лямбду:
std::shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });⚡️Ну или воспользоваться уже готовым вариантом:
std::shared_ptr<int> sp(new int[10], std::default_delete<int[]>());
std::default_delete имеет частичную специализацию для массивов.
Но! Какой хороший все-таки стандарт С++17, который поправил многие такие маленькие косячки. А как он это сделал - увидим в следующий раз)
Be comfortable to work with. Stay cool.
#cpp11 #memory
👍12🔥10❤4
Исправляем косяк std::shared_ptr с массивами
Ну не мы сами, конечно. Стандарт С++17 исправляет этот момент.
Что мы теперь имеем.
Для создания объекта таким конструктором:
используется делитер delete ptr, если T - не массив, и delete[] ptr если Т -массив.
Также теперь изменился тип хранимого объекта element_type. Раньше был просто шаблонный тип Т, теперь же это
std::remove_extent - это такой type_trait. Все, что нужно о нем знать - если Т - массив, то тип element_type будет совпадать с типом элементов массива.
Теперь мы даже можем использовать operator[] для доступа к элементам массива. Делается это так:
Так что теперь это действительно полноценные шареные массивы из коробки. Весь интерфейс подогнали под это дело.
Но вот вопрос: а нафига это вообще надо? Когда кто-то вообще в последний раз использовал динамический массив?
Мы же вроде на плюсах пишем. Есть плюсовые решения - std::vector, если размер не известен на момент компиляции, и std::array, если известен. У них и интерфейс удобный и унифицированный и все-таки это объектно-ориентированный подход. И сердцу тепло, и глаз радуется. Динамические массивы выглядят, как окаменелые какашки динозавров.
C std::array соглашусь. Думаю, что нет адекватных оправданий использования динамических и статических массивов, длина которых известна в compile-time. std::array - очень простая и тонкая обертка над статическим массивом и ее использование вырождается компилятором до использования массива.
Но вот с векторами немного сложнее. Удобство требует жертв. Именно в плане производительности. Поэтому в узких бутылочных горлышках, где надо выжимать всю скорость из кода, лучше использовать динамические массивы вместо std::vector. Видел запрос от Захара на пример, который подверждает эту мысль. Отвечу на него в другом посте как-нибудь. Но обычному бэкэндеру, думаю, это сильно не пригодится.
Если фича есть, значит она кому-то нужна. Просто иногда интересно узнать о таких минорных изменениях. А кому-то поможет больше не использовать кастомные делитеры и иметь более понятный код.
Fix your flaws. Stay cool.
#cpp17 #memory
Ну не мы сами, конечно. Стандарт С++17 исправляет этот момент.
Что мы теперь имеем.
Для создания объекта таким конструктором:
template< class T >
explicit shared_ptr( T* ptr );
используется делитер delete ptr, если T - не массив, и delete[] ptr если Т -массив.
Также теперь изменился тип хранимого объекта element_type. Раньше был просто шаблонный тип Т, теперь же это
using element_type = remove_extent_t<T>;
std::remove_extent - это такой type_trait. Все, что нужно о нем знать - если Т - массив, то тип element_type будет совпадать с типом элементов массива.
Теперь мы даже можем использовать operator[] для доступа к элементам массива. Делается это так:
std::shared_ptr<int[]> num(new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
for (std::size_t i = 0; i < 10; ++i)
std::cout << num[i] << ' ';Так что теперь это действительно полноценные шареные массивы из коробки. Весь интерфейс подогнали под это дело.
Но вот вопрос: а нафига это вообще надо? Когда кто-то вообще в последний раз использовал динамический массив?
Мы же вроде на плюсах пишем. Есть плюсовые решения - std::vector, если размер не известен на момент компиляции, и std::array, если известен. У них и интерфейс удобный и унифицированный и все-таки это объектно-ориентированный подход. И сердцу тепло, и глаз радуется. Динамические массивы выглядят, как окаменелые какашки динозавров.
C std::array соглашусь. Думаю, что нет адекватных оправданий использования динамических и статических массивов, длина которых известна в compile-time. std::array - очень простая и тонкая обертка над статическим массивом и ее использование вырождается компилятором до использования массива.
Но вот с векторами немного сложнее. Удобство требует жертв. Именно в плане производительности. Поэтому в узких бутылочных горлышках, где надо выжимать всю скорость из кода, лучше использовать динамические массивы вместо std::vector. Видел запрос от Захара на пример, который подверждает эту мысль. Отвечу на него в другом посте как-нибудь. Но обычному бэкэндеру, думаю, это сильно не пригодится.
Если фича есть, значит она кому-то нужна. Просто иногда интересно узнать о таких минорных изменениях. А кому-то поможет больше не использовать кастомные делитеры и иметь более понятный код.
Fix your flaws. Stay cool.
#cpp17 #memory
🔥8❤6👍3😁1
std::make_shared в С++20
Начиная со стандарта С++11 в С++ появилась поддержка создания std::shared_ptr при помощи фабричной функции std::make_shared. У нас даже есть пост про особенности этой функции вот здесь. Но у нее были такие же недостатки, как и у std::shared_ptr до С++17. Нельзя было ее использовать для массивов. Но, как отметил уже в комментах Константин, начиная с С++20 эта фабричная функция синхронизировалась со своим вдохновителем и теперь тоже поддерживает создание массивов из std::shared_ptr. Например:
⚡️ std::shared_ptr<double[]> shar = std::make_shared<double[]>(1024): создает std::shared_ptr c 1024 значениями типа double, проинициализированными по умолчанию;
⚡️ std::shared_ptr<double[]> shar = std::make_shared<double[]>(1024, 1.0): создает std::shared_ptr c 1024 значениями типа double, проинициализированными значениями, равными 1,0.
Как обычно make функции немного тормозят относительно типов, для которых они созданы. Типа std::make_unique появился только в с++14, хотя сам уникальный указатель был представлен в предыдущем релизе. Но главное, что эти особенности все-таки доезжают, что не может не радовать.
Enjoy small things. Stay cool.
#cpp20 #memory
Начиная со стандарта С++11 в С++ появилась поддержка создания std::shared_ptr при помощи фабричной функции std::make_shared. У нас даже есть пост про особенности этой функции вот здесь. Но у нее были такие же недостатки, как и у std::shared_ptr до С++17. Нельзя было ее использовать для массивов. Но, как отметил уже в комментах Константин, начиная с С++20 эта фабричная функция синхронизировалась со своим вдохновителем и теперь тоже поддерживает создание массивов из std::shared_ptr. Например:
⚡️ std::shared_ptr<double[]> shar = std::make_shared<double[]>(1024): создает std::shared_ptr c 1024 значениями типа double, проинициализированными по умолчанию;
⚡️ std::shared_ptr<double[]> shar = std::make_shared<double[]>(1024, 1.0): создает std::shared_ptr c 1024 значениями типа double, проинициализированными значениями, равными 1,0.
Как обычно make функции немного тормозят относительно типов, для которых они созданы. Типа std::make_unique появился только в с++14, хотя сам уникальный указатель был представлен в предыдущем релизе. Но главное, что эти особенности все-таки доезжают, что не может не радовать.
Enjoy small things. Stay cool.
#cpp20 #memory
🔥11👍3❤1
Единица трансляции
В недавнем прошлом на канале было много линковочной тематики. Соответственно это требует частого упоминания термина "единица трансляции" aka translation unit aka юнит. На канале много не нюхавшего прода народа, поэтому пришла в голову идея закрыть этот гештальт.
Вот вы изучаете плюсы. Долго и упорно. И сделали свой первый нетривиальный проект. И бежите радостный показывать его маме со словами: "Мама! Смотри, какую я программу написал.". И показываете ей, как ваши условные крестики-нолики работают. Это вы запустили исполняемый файл, который является результатом превращения вашего кода на с++ в машинный код. Но каким образом все многообразие файлов собирается в одну сущность? Как они между собой взаимодействуют для этого?
В рамках культуры разработки на С++ есть 2 принципиальных вида файлов: headers(заголовочные файлы)(.h|.hpp) и sources(файлы реализации)(.cxx|.cpp). Первые обычно предназначены для помещения в них сущностей, которые будут широко(или потенциально широко) использоваться в проекте. Вторые уже конкретно реализуют поведение этих сущностей. Ответ на вопросы: "Зачем такое разделение в принципе есть?" и "Зачем мы пишем множество файлов реализации?"- заслуживают отдельных постов. Просто примем за данность всем известный факт существования двух видов файлов.
Но на вход компилятора попадает только один тип файлов - sources(не будем касаться precompiled headers и сильно усложнять разговор). На самом деле компилятора может сожрать любой подходящий файл, просто так принято, что это .cpp файлы.
Стандартный source файл состоит из кучи заинклюженых заголовочников, возможно еще несколько видов директив препроцессора, типа #ifdef или #define и прочего, ну и, собственно, нашего кода. Когда такой файл пожирает компилятор, на самом деле первым в работу вступает препроцессор. Это такой предварительный обработчик файлов с кодом. В С и С++ есть несколько директив препроцессора, которые предназначены именно для этого обработчика. Он оперирует в основном текстом программы и фактически делает текстовые манипуляции. Например, директива #include"something.hpp" заменяется на полный текст файла something.hpp. Директива #define MAXARRAYSIZE 10 обозначает, что во всем файле строку MAXARRAYSIZE нужно заменить на 10. Причем 10 это будет прям строка в файле, это надо учитывать. Никакой проверки типов на этом этапе нет.
И вот файл, который получается после обработки исходника препроцессором называется единицей трансляции. То есть это базовый элемент компиляции С++ программы. Компилятор обрабатывает все единицы трансляции, превращая их в объектные файлы. И уже линкер собирает все эти объектные файлы в одну программу.
То есть и вправду не очень корректно говорить, что компилируются цппшники. Это делают все-таки единицы трансляции. Но я не оч понимаю людей, которые хейтят других за это, потому что просто культурой принято файлы реализации называть .cpp. А то, что там есть препроцессор - это и так понятно. Это понимает почти любой, кто писал #include <iostream>, а то есть почти все. Я уж преувеличиваю, но вы поняли мою мысль.
Divide et impera. Stay cool.
#compiler #cppcore
В недавнем прошлом на канале было много линковочной тематики. Соответственно это требует частого упоминания термина "единица трансляции" aka translation unit aka юнит. На канале много не нюхавшего прода народа, поэтому пришла в голову идея закрыть этот гештальт.
Вот вы изучаете плюсы. Долго и упорно. И сделали свой первый нетривиальный проект. И бежите радостный показывать его маме со словами: "Мама! Смотри, какую я программу написал.". И показываете ей, как ваши условные крестики-нолики работают. Это вы запустили исполняемый файл, который является результатом превращения вашего кода на с++ в машинный код. Но каким образом все многообразие файлов собирается в одну сущность? Как они между собой взаимодействуют для этого?
В рамках культуры разработки на С++ есть 2 принципиальных вида файлов: headers(заголовочные файлы)(.h|.hpp) и sources(файлы реализации)(.cxx|.cpp). Первые обычно предназначены для помещения в них сущностей, которые будут широко(или потенциально широко) использоваться в проекте. Вторые уже конкретно реализуют поведение этих сущностей. Ответ на вопросы: "Зачем такое разделение в принципе есть?" и "Зачем мы пишем множество файлов реализации?"- заслуживают отдельных постов. Просто примем за данность всем известный факт существования двух видов файлов.
Но на вход компилятора попадает только один тип файлов - sources(не будем касаться precompiled headers и сильно усложнять разговор). На самом деле компилятора может сожрать любой подходящий файл, просто так принято, что это .cpp файлы.
Стандартный source файл состоит из кучи заинклюженых заголовочников, возможно еще несколько видов директив препроцессора, типа #ifdef или #define и прочего, ну и, собственно, нашего кода. Когда такой файл пожирает компилятор, на самом деле первым в работу вступает препроцессор. Это такой предварительный обработчик файлов с кодом. В С и С++ есть несколько директив препроцессора, которые предназначены именно для этого обработчика. Он оперирует в основном текстом программы и фактически делает текстовые манипуляции. Например, директива #include"something.hpp" заменяется на полный текст файла something.hpp. Директива #define MAXARRAYSIZE 10 обозначает, что во всем файле строку MAXARRAYSIZE нужно заменить на 10. Причем 10 это будет прям строка в файле, это надо учитывать. Никакой проверки типов на этом этапе нет.
И вот файл, который получается после обработки исходника препроцессором называется единицей трансляции. То есть это базовый элемент компиляции С++ программы. Компилятор обрабатывает все единицы трансляции, превращая их в объектные файлы. И уже линкер собирает все эти объектные файлы в одну программу.
То есть и вправду не очень корректно говорить, что компилируются цппшники. Это делают все-таки единицы трансляции. Но я не оч понимаю людей, которые хейтят других за это, потому что просто культурой принято файлы реализации называть .cpp. А то, что там есть препроцессор - это и так понятно. Это понимает почти любой, кто писал #include <iostream>, а то есть почти все. Я уж преувеличиваю, но вы поняли мою мысль.
Divide et impera. Stay cool.
#compiler #cppcore
🔥15👍5❤3
static функции
В этом посте были краткие выжимки из того, как ключевое слово static влияет на сущности. Сегодня будем разбирать функции.
Для начала надо понимать базовые настройки функции, чтобы отталкиваться от этого в контексте static.
Функция - блок кода в .text section, то есть просто в области, где находится код. Этому куску кода соответствует определенная метка - замангленное имя функции(видимо уже пора делать пост про манглинг, а то много упоминаний без объяснений). Когда функцию хотят вызвать, то это делается через инструкцию call, которая принимает метку функции. Этой метке после линковки будет соответствовать конкретный адрес, которому и будет передано исполнение кода во время выполнения программы.
Mangled name функции формируется только на основе ее сигнатуры. Поэтому любой код, который знает только лишь(!) сигнатуру функции, то есть ее объявление, знает трушное название функции(ту самую метку). Вот теперь интересности.
По дефолту функции имеют внешнее связывание.
Для текущей единицы трансляции все тривиально. Есть метка, мы можем просто перейти на нее.
Но внешнее связывание значит, что и другие единицы трансляции могут видеть эту функцию, не зная ее определение! Не только видеть, но и вызвать! Как? Имея правильное объявление функции, текущая единица трансляции получает доступ к замангленному имени функции. А в коде появится такая строчка: call label. Прикол в том, что до этапа линковки мы можем пытаться в коде вызывать вообще любые функции и нам это будет сходить с рук. А вот уже работа линкера заключается в том, чтобы сопоставить метку из вызова с адресом реальной функции. И если линкер найдет код для этой метки в другой единице трансляции, то он просто подставит адрес, соответствующий метке, в call и все будет чики-пуки.
Ну и для того, чтобы линкер в принципе смог определить, что текущую метку могут видеть все остальные единицы трансляции, ее надо пометить как .globl label. Логично предположить, что так обозначаются глобальные для всей программы сущности, коей и является базовая функция.
Я описываю все сильно верхнеуровнево(насколько это возможно, обсуждая ассемблер ахха). Но вроде должно быть понятно.
Теперь вернемся к нашим static баранам. Что тут на самом деле меняется. Сильно верхнеуровнего - меняется тип связывания с внешнего на внутреннее. Это значит, что другие единицы трансляции просто перестают видеть эту функцию. Звучит прикольно, но как конкретно это изменение достигается?
На самом деле всего двумя деталями.
1) Пометка .globl label больше не генерируется.
2) Появляется заглавная L перед именем функции(которое в с++ коде было) в ее замангленном варианте.
Что это дает. Даже если мы знаем сигнатуру функции и объявили ее в другой единице трансляции, то на этапе линковки компоновщик посмотрит на реальное определение функции, не увидит пометку о глобальности символа, распарсит замангленное имя и увидит эту букву L и поймет, что это локальная функция для этой единицы трансляции. И не будет резолвить этот символ. Если линкер не найдет подходящего глобального определения в остальных юнитах трансляции, то произойдет ошибка линковки - undefined reference.
И на самом деле, локальная видимость функции открывает дорогу к некоторым оптимизациям. Например, компилятор может решить, что функция подходит для inline expantion и встроить все ее вызовы. Но раз в текущем юните код функции не нужен(его полностью встроили везде, где требуется), а в других его никто не должен видеть, то компилятор просто удалит метку этой функции и ее сгенерированный код. Это позволяет уменьшить размер бинаря. Мы конечно его увеличиваем за счет встраивания кода функции. Но лучше так, чем оставлять бесполезный код в бинарнике.
Hide your secrets. Stay cool.
#compiler #cppcore #optimization
В этом посте были краткие выжимки из того, как ключевое слово static влияет на сущности. Сегодня будем разбирать функции.
Для начала надо понимать базовые настройки функции, чтобы отталкиваться от этого в контексте static.
Функция - блок кода в .text section, то есть просто в области, где находится код. Этому куску кода соответствует определенная метка - замангленное имя функции(видимо уже пора делать пост про манглинг, а то много упоминаний без объяснений). Когда функцию хотят вызвать, то это делается через инструкцию call, которая принимает метку функции. Этой метке после линковки будет соответствовать конкретный адрес, которому и будет передано исполнение кода во время выполнения программы.
Mangled name функции формируется только на основе ее сигнатуры. Поэтому любой код, который знает только лишь(!) сигнатуру функции, то есть ее объявление, знает трушное название функции(ту самую метку). Вот теперь интересности.
По дефолту функции имеют внешнее связывание.
Для текущей единицы трансляции все тривиально. Есть метка, мы можем просто перейти на нее.
Но внешнее связывание значит, что и другие единицы трансляции могут видеть эту функцию, не зная ее определение! Не только видеть, но и вызвать! Как? Имея правильное объявление функции, текущая единица трансляции получает доступ к замангленному имени функции. А в коде появится такая строчка: call label. Прикол в том, что до этапа линковки мы можем пытаться в коде вызывать вообще любые функции и нам это будет сходить с рук. А вот уже работа линкера заключается в том, чтобы сопоставить метку из вызова с адресом реальной функции. И если линкер найдет код для этой метки в другой единице трансляции, то он просто подставит адрес, соответствующий метке, в call и все будет чики-пуки.
Ну и для того, чтобы линкер в принципе смог определить, что текущую метку могут видеть все остальные единицы трансляции, ее надо пометить как .globl label. Логично предположить, что так обозначаются глобальные для всей программы сущности, коей и является базовая функция.
Я описываю все сильно верхнеуровнево(насколько это возможно, обсуждая ассемблер ахха). Но вроде должно быть понятно.
Теперь вернемся к нашим static баранам. Что тут на самом деле меняется. Сильно верхнеуровнего - меняется тип связывания с внешнего на внутреннее. Это значит, что другие единицы трансляции просто перестают видеть эту функцию. Звучит прикольно, но как конкретно это изменение достигается?
На самом деле всего двумя деталями.
1) Пометка .globl label больше не генерируется.
2) Появляется заглавная L перед именем функции(которое в с++ коде было) в ее замангленном варианте.
Что это дает. Даже если мы знаем сигнатуру функции и объявили ее в другой единице трансляции, то на этапе линковки компоновщик посмотрит на реальное определение функции, не увидит пометку о глобальности символа, распарсит замангленное имя и увидит эту букву L и поймет, что это локальная функция для этой единицы трансляции. И не будет резолвить этот символ. Если линкер не найдет подходящего глобального определения в остальных юнитах трансляции, то произойдет ошибка линковки - undefined reference.
И на самом деле, локальная видимость функции открывает дорогу к некоторым оптимизациям. Например, компилятор может решить, что функция подходит для inline expantion и встроить все ее вызовы. Но раз в текущем юните код функции не нужен(его полностью встроили везде, где требуется), а в других его никто не должен видеть, то компилятор просто удалит метку этой функции и ее сгенерированный код. Это позволяет уменьшить размер бинаря. Мы конечно его увеличиваем за счет встраивания кода функции. Но лучше так, чем оставлять бесполезный код в бинарнике.
Hide your secrets. Stay cool.
#compiler #cppcore #optimization
👍17🔥8❤4
Объединения условий в enable_if
Иногда мы хотим сильно ограничить свойства типов, с которыми мы хотим инстанцировать шаблон. Например, тип должен быть default-constructed и иметь оператор сравнения. У нас есть для этого метафункция std::enable_if, которая позволяет нам проверять наличие свойств у типов. Но вот незадача, как проверить два условия одновременно? Я хочу и то, и то.
Ранее для этого использовались обычные операторы && и || между тайптрейтами.
Однако в С++17 появились специальные метаклассы, которые позволяют комбинировать условия. Это
• template<class... B> struct conjunction; - логическое И
• template<class... B> struct disjunction; - логическое ИЛИ
• template<class B> struct negation; - логичесткое НЕ
Эти трейты имеют подходящие осмысленные имена, поэтому их использование повышает читаемость кода.
Например, вы можете сочетать эти метафункции с variadic шаблонами
Тогда может появиться что-то такое:
В функцию PrintIntegers мы можем передать сколько угодно(почти) аргументов и все они будут проверяться на соответствие целочисленному типу
Или например вы хотите принтовать только числа? Тогда мы идем к вам:
Если тип - не целочисленный и не с плавающей точкой, то такая перегрузка будет отбрасываться.
Ну и например вы хотите, чтобы функция для вывода в консоль не принимала аргумент-указатель. Тоже можно сделать.
Такие вот удобные метафункции.
Create complex conditions. Stay cool.
#cpp17 #template
Иногда мы хотим сильно ограничить свойства типов, с которыми мы хотим инстанцировать шаблон. Например, тип должен быть default-constructed и иметь оператор сравнения. У нас есть для этого метафункция std::enable_if, которая позволяет нам проверять наличие свойств у типов. Но вот незадача, как проверить два условия одновременно? Я хочу и то, и то.
Ранее для этого использовались обычные операторы && и || между тайптрейтами.
Однако в С++17 появились специальные метаклассы, которые позволяют комбинировать условия. Это
• template<class... B> struct conjunction; - логическое И
• template<class... B> struct disjunction; - логическое ИЛИ
• template<class B> struct negation; - логичесткое НЕ
Эти трейты имеют подходящие осмысленные имена, поэтому их использование повышает читаемость кода.
Например, вы можете сочетать эти метафункции с variadic шаблонами
Тогда может появиться что-то такое:
template<typename... Ts>
std::enable_if_t<std::conjunction_v<std::is_same<int, Ts>...>> PrintIntegers(Ts ... args)
{
(std::cout << ... << args) << 'n';
}
В функцию PrintIntegers мы можем передать сколько угодно(почти) аргументов и все они будут проверяться на соответствие целочисленному типу
Или например вы хотите принтовать только числа? Тогда мы идем к вам:
template<typename T, typename = std::enable_if_t<std::disjunction_v<std::is_integral<T>, std::is_floating_point<T>>>>
void PrintValue(T value)
{
std::cout << "Value: " << value << std::endl;
}
Если тип - не целочисленный и не с плавающей точкой, то такая перегрузка будет отбрасываться.
Ну и например вы хотите, чтобы функция для вывода в консоль не принимала аргумент-указатель. Тоже можно сделать.
template<typename T, typename = std::enable_if_t<std::negation_v<std::is_pointer<T>>>>
void PrintValue(T value)
{
std::cout << "Value: " << value << std::endl;
}
Такие вот удобные метафункции.
Create complex conditions. Stay cool.
#cpp17 #template
👍22🔥8❤4😁2🌭1
Вычисления по короткой схеме
Базовое и очень важное понятие для программирования в принципе и на плюсах в частности. Встретил просто английский термин short-circuit evaluation и понял, что, в целом, тема достойна поста.
Вычисления по короткой схеме, также известны как вычисления Маккарти — это стратегия вычисления в некоторых языках программирования, при которой второй логический оператор выполняется или вычисляется только в том случае, если первого логического оператора недостаточно для определения значения выражения. Таким образом, после того, как результат выражения становится очевидным, его вычисление прекращается.
Посмотрим, что это значит.
В плюсах есть два логических оператора, которые работают по этому признаку - && и ||. Логические И и ИЛИ.
Когда мы пишем if (expression1 && expression2) это значит, что сначала вычисляется expression1 и смотрится его значение. Если оно приводится к false, то результат всего составного условия - false. А expression2 даже не вычисляется. Если expression1 приводится к true, то вычисляем expression2 и уже его значение определит результат. Такое поведение вполне понятно. Выражение с логическим И истинно тогда и только тогда, когда истинны оба операнда. А если один из них ложный - тогда и все выражение ложно. Тогда нет смысла тратить время на вычисление expression2, если оно никак не повлияет на результат операции.
По аналогии работает оператор ИЛИ. Когда мы пишем if (expression1 || expression2) это значит, что сначала вычисляется expression1 и смотрится его значение. Если оно приводится к true, то результат всего составного условия - true. И, естественно, expression2 даже не вычисляется. Если expression1 приводится к false, то вычисляем expression2 и уже его значение определит результат. Все опять же исходит от определения. Выражение с логическим ИЛИ ложно тогда и только тогда, когда ложны оба операнда. А если один из них истинный - тогда и все выражение истинно. (Немного копипасты, но, надеюсь, вы выдержали).
Если немного обобщить, то в выражениях вида p1 && p2 && p3... либо p1 || p2 || p3 … вычисление продолжается слева направо, пока очередной операнд не даст false или true соответственно.
Почему это вообще важно?
Дело даже не в том, что мы сохраняем время на, по сути, ненужные вычисления. Безусловно, это кейс использования, но по моему мнению не самый важный.
Действительно важный кейс, без которого было бы сложно - первое выражение выступает как precondition для второго. Например, первое выражение проверяет параметр на равенство нулю, а второе выражение использует этот параметр в качестве делителя. Только тогда, когда параметр ненулевой, мы сможем вычислить деление. А когда нулевой, мы даже не приступим к делению. Такое условие обезопасит нас от банальной ошибки деления на ноль. Также очень часто проверки касаются границ массива. Если индекс в пределах размера массива, то можем его использовать дальше.
На этом правиле основано большинство составных условий, поэтому критически важно знать это правило, чтобы полностью понимать замысел автора кода.
Use preconditions for making important choice. Stay cool.
#cppcore
Базовое и очень важное понятие для программирования в принципе и на плюсах в частности. Встретил просто английский термин short-circuit evaluation и понял, что, в целом, тема достойна поста.
Вычисления по короткой схеме, также известны как вычисления Маккарти — это стратегия вычисления в некоторых языках программирования, при которой второй логический оператор выполняется или вычисляется только в том случае, если первого логического оператора недостаточно для определения значения выражения. Таким образом, после того, как результат выражения становится очевидным, его вычисление прекращается.
Посмотрим, что это значит.
В плюсах есть два логических оператора, которые работают по этому признаку - && и ||. Логические И и ИЛИ.
Когда мы пишем if (expression1 && expression2) это значит, что сначала вычисляется expression1 и смотрится его значение. Если оно приводится к false, то результат всего составного условия - false. А expression2 даже не вычисляется. Если expression1 приводится к true, то вычисляем expression2 и уже его значение определит результат. Такое поведение вполне понятно. Выражение с логическим И истинно тогда и только тогда, когда истинны оба операнда. А если один из них ложный - тогда и все выражение ложно. Тогда нет смысла тратить время на вычисление expression2, если оно никак не повлияет на результат операции.
По аналогии работает оператор ИЛИ. Когда мы пишем if (expression1 || expression2) это значит, что сначала вычисляется expression1 и смотрится его значение. Если оно приводится к true, то результат всего составного условия - true. И, естественно, expression2 даже не вычисляется. Если expression1 приводится к false, то вычисляем expression2 и уже его значение определит результат. Все опять же исходит от определения. Выражение с логическим ИЛИ ложно тогда и только тогда, когда ложны оба операнда. А если один из них истинный - тогда и все выражение истинно. (Немного копипасты, но, надеюсь, вы выдержали).
Если немного обобщить, то в выражениях вида p1 && p2 && p3... либо p1 || p2 || p3 … вычисление продолжается слева направо, пока очередной операнд не даст false или true соответственно.
Почему это вообще важно?
Дело даже не в том, что мы сохраняем время на, по сути, ненужные вычисления. Безусловно, это кейс использования, но по моему мнению не самый важный.
Действительно важный кейс, без которого было бы сложно - первое выражение выступает как precondition для второго. Например, первое выражение проверяет параметр на равенство нулю, а второе выражение использует этот параметр в качестве делителя. Только тогда, когда параметр ненулевой, мы сможем вычислить деление. А когда нулевой, мы даже не приступим к делению. Такое условие обезопасит нас от банальной ошибки деления на ноль. Также очень часто проверки касаются границ массива. Если индекс в пределах размера массива, то можем его использовать дальше.
На этом правиле основано большинство составных условий, поэтому критически важно знать это правило, чтобы полностью понимать замысел автора кода.
Use preconditions for making important choice. Stay cool.
#cppcore
🔥24👍10❤4
Подробности про std::conjunction vs &&
В этом посте я рассказывал про замечательные тайптрейты std::conjunction, std::disjunction. Они позволяют компоновать несколько трейтов в одну логическую последовательность. Там же я рассказывал про то, что до них для этих целей использовались операторы &&, ||. Безусловно, человеческим языком обозванные сущности проще воспринимаются, чем какие-то символы. Но неужели это все различия? Какая-то вялая причина, чтобы вводить в стандарт эти трейты.
И правда, различия есть. Еще какие!
Прежде, чем начать разбирать их, нужно поподробнее рассмотреть эти метаклассы, потому что оттуда все различия. Рассматривать будем на примере std::conjunction, ибо у них все очень похоже.
Примерно так этот класс может быть реализован
Специализация std::conjunction<B1, ..., Bn> имеет публичную базу, которая варьируется в зависимости от аргументов
Если их нет, то базовым классом для std::conjunction будет std::true_type.
Если они есть, то базой будет первый тип Bi из B1, ..., Bn, для которого bool(Bi::value) == false.
Если для всех Bi bool(Bi::value) == true, тогда базой будет Bn.
Кстати std::conjunction не обязательно по итогу наследуется либо от std::true_type, либо от std::false_type: она просто наследует от первого B, для которого его ::value, явно преобразованное в bool, является ложным, или от самого последнего B, когда все они преобразуются в true. То есть это самое value может быть даже не булевым значением, а например числом. Вот так:
И вот в этом весь прикол. std::conjunction - это вычисление по короткой схеме! То есть как только мы нашли такой Bi, что для него bool(Bi::value) == false, компилятор прекращает дальше инстанцировать вглубь рекурсии и однозначно определяет тип базового класса, а значит и поля value.
И как раз таки в этом аспекте метаклассы conjunction и disjunction отличаются от обычных && и ||. Но об этом мы поговорим завтра.
Understand true essence of things. Stay cool.
#cpp17 #template #hardcore
В этом посте я рассказывал про замечательные тайптрейты std::conjunction, std::disjunction. Они позволяют компоновать несколько трейтов в одну логическую последовательность. Там же я рассказывал про то, что до них для этих целей использовались операторы &&, ||. Безусловно, человеческим языком обозванные сущности проще воспринимаются, чем какие-то символы. Но неужели это все различия? Какая-то вялая причина, чтобы вводить в стандарт эти трейты.
И правда, различия есть. Еще какие!
Прежде, чем начать разбирать их, нужно поподробнее рассмотреть эти метаклассы, потому что оттуда все различия. Рассматривать будем на примере std::conjunction, ибо у них все очень похоже.
Примерно так этот класс может быть реализован
template<class...> struct conjunction : std::true_type
template<class B1> struct conjunction<B1> : B1 {};
template<class B1, class... Bn>
struct conjunction<B1, Bn...>
: std::conditional_t<bool(B1::value), conjunction<Bn...>, B1> {};
Специализация std::conjunction<B1, ..., Bn> имеет публичную базу, которая варьируется в зависимости от аргументов
Если их нет, то базовым классом для std::conjunction будет std::true_type.
Если они есть, то базой будет первый тип Bi из B1, ..., Bn, для которого bool(Bi::value) == false.
Если для всех Bi bool(Bi::value) == true, тогда базой будет Bn.
Кстати std::conjunction не обязательно по итогу наследуется либо от std::true_type, либо от std::false_type: она просто наследует от первого B, для которого его ::value, явно преобразованное в bool, является ложным, или от самого последнего B, когда все они преобразуются в true. То есть это самое value может быть даже не булевым значением, а например числом. Вот так:
std::conjunction<std::integral_constant<int, 2>,std::integral_constant<int, 4>>::value == 4 - верно!
И вот в этом весь прикол. std::conjunction - это вычисление по короткой схеме! То есть как только мы нашли такой Bi, что для него bool(Bi::value) == false, компилятор прекращает дальше инстанцировать вглубь рекурсии и однозначно определяет тип базового класса, а значит и поля value.
И как раз таки в этом аспекте метаклассы conjunction и disjunction отличаются от обычных && и ||. Но об этом мы поговорим завтра.
Understand true essence of things. Stay cool.
#cpp17 #template #hardcore
👍15❤6🔥6❤🔥1
std::conjunction vs &&
Вчера мы поговорили о том, что инстанцирование в std::conjunction и std::disjunction происходит по короткой схеме. Как только мы нашли шаблонный аргумент, для которого будет валидно выражение bool(Bi::value) == false, инстанциация остальных аргументов прекращается и выводится тип std::conjunction::value.
Однако при инстанциации шаблонов операторы && и || не могут похвастаться наличием короткой схемы. Что в форме fold expression, что в явной последовательной форме. То есть
компиляция вот этого кода закончится с ошибкой error: value is not a member of type_without_value<int>. Хотя float - совсем не интергральный тип и при работающей короткой схеме вычислений, ошибки компиляции не было бы. Потому что второй тип даже не инстанцировался бы. Как в случае с std::conjunction.
Такой код успешно скомпилируется и result будет равен false. Потому что инстанциация первого шаблона valid_except_void<T1> завершится успешно и для него bool(value) == false. Поэтому validexceptvoid с войдом не испортит нам игру.
То есть преимущество std::conjunction в том, что как только мы нашли его аргумент, для которого bool(value) == false, то все последующие аргументы не инстанцируются. Это может быть полезно, когда последующие типы очень затратные при инстанцировании или могут вызвать фатальные ошибки, если их инстанцировать с неправильным типом.
Прошу прощение за частое употребление слова "инстанцировать". Я просто не знаю нормального аналога/синонима. Кто знает, поделитесь в комментах.
Для ценителей метапрограммирования оставляю ссылку на годболт, чтобы вы могли поиграться с кодом.
Always compare your tools. Stay cool.
#template #cpp17
Вчера мы поговорили о том, что инстанцирование в std::conjunction и std::disjunction происходит по короткой схеме. Как только мы нашли шаблонный аргумент, для которого будет валидно выражение bool(Bi::value) == false, инстанциация остальных аргументов прекращается и выводится тип std::conjunction::value.
Однако при инстанциации шаблонов операторы && и || не могут похвастаться наличием короткой схемы. Что в форме fold expression, что в явной последовательной форме. То есть
template <class T>
struct type_without_value
{
};
template <class T1, class T2>
constexpr auto numbers = (std::is_integral_v<T1> && type_without_value<T2>::value);
constexpr auto result = numbers<float, int>;
компиляция вот этого кода закончится с ошибкой error: value is not a member of type_without_value<int>. Хотя float - совсем не интергральный тип и при работающей короткой схеме вычислений, ошибки компиляции не было бы. Потому что второй тип даже не инстанцировался бы. Как в случае с std::conjunction.
template <typename T>
struct valid_except_void : std::false_type
{
};
template <>
struct valid_except_void<void>
{
};
template <class T1, class T2>
constexpr auto test = std::conjunction_v<valid_except_void<T1>, valid_except_void<T2>>;
constexpr auto result = test<float, void>;
Такой код успешно скомпилируется и result будет равен false. Потому что инстанциация первого шаблона valid_except_void<T1> завершится успешно и для него bool(value) == false. Поэтому validexceptvoid с войдом не испортит нам игру.
То есть преимущество std::conjunction в том, что как только мы нашли его аргумент, для которого bool(value) == false, то все последующие аргументы не инстанцируются. Это может быть полезно, когда последующие типы очень затратные при инстанцировании или могут вызвать фатальные ошибки, если их инстанцировать с неправильным типом.
Прошу прощение за частое употребление слова "инстанцировать". Я просто не знаю нормального аналога/синонима. Кто знает, поделитесь в комментах.
Для ценителей метапрограммирования оставляю ссылку на годболт, чтобы вы могли поиграться с кодом.
Always compare your tools. Stay cool.
#template #cpp17
🔥13👍5❤4
Сумма трех
Надеюсь, что вы и ваши близкие живы и здоровы. Чтобы начать новую неделю со свежей головой и отвлеченными мыслями, предлагаю порешать задачку.
Есть знаменитая в кругах решателей алгосов задача - сумма двух. Решается просто, понимается просто. Идеальная задачка для начинающих. Но сегодня я задам вам похожий по формулировке, но уже не такой простой вопрос.
Дан массив интов arr. Как найти в этом массиве триплет {arr[i], arr[j], arr[k]}, где i != j, i != k, j != k, такой что arr[i] + arr[j] + arr[k] = 0? Напишите функцию, которая возвращает все такие триплеты. Если их нет, то верните пустой массив.
Очень важно заметить, что в ответе не принимаются дубликаты триплетов. То есть не только индексы не должны повторяться, но и сами наборы чисел в тройках. Это значит, что даже банальным брут форсом простыми тремя циклами без дополнительной памяти вы не сможете решить.
Напомню про формат: под этим постом пишут совсем зеленые, кто не знает, правильно ли они решают или нет. Под следующим постом мы разбираем конкретные решения людей, которые желают получить фитбэк. Если вы точно знаете ответ и хотите его сказать, то прошу воздержаться от комментариев. Если они все же останутся, то оставьте их под постом с решением, который выйдет вечером.
Погнали решать!
Challenge yourself. Stay cool.
#задачки
Надеюсь, что вы и ваши близкие живы и здоровы. Чтобы начать новую неделю со свежей головой и отвлеченными мыслями, предлагаю порешать задачку.
Есть знаменитая в кругах решателей алгосов задача - сумма двух. Решается просто, понимается просто. Идеальная задачка для начинающих. Но сегодня я задам вам похожий по формулировке, но уже не такой простой вопрос.
Дан массив интов arr. Как найти в этом массиве триплет {arr[i], arr[j], arr[k]}, где i != j, i != k, j != k, такой что arr[i] + arr[j] + arr[k] = 0? Напишите функцию, которая возвращает все такие триплеты. Если их нет, то верните пустой массив.
Очень важно заметить, что в ответе не принимаются дубликаты триплетов. То есть не только индексы не должны повторяться, но и сами наборы чисел в тройках. Это значит, что даже банальным брут форсом простыми тремя циклами без дополнительной памяти вы не сможете решить.
Напомню про формат: под этим постом пишут совсем зеленые, кто не знает, правильно ли они решают или нет. Под следующим постом мы разбираем конкретные решения людей, которые желают получить фитбэк. Если вы точно знаете ответ и хотите его сказать, то прошу воздержаться от комментариев. Если они все же останутся, то оставьте их под постом с решением, который выйдет вечером.
Погнали решать!
Challenge yourself. Stay cool.
#задачки
❤10🔥6👍4😱1
Fold expressions. Мотивация.
Стандарт C++11 привнес в нашу жизнь замечательную фичу - variadic templates, которая является очень мощным инструментом в метапрограммировании. Она используется, когда нам необходимо написать функцию, которая принимает неопределенное количество аргументов. Ранее такой возможности в С++ не было (имею ввиду типобезопасные шаблонные функции) и приходилось отдельно специфицировать функцию в начале с одним аргументом, потом с двумя, потом с тремя и так далее, пока не надоест, не настанет обед или больше не нужно будет. Не очень удобненько.
Однако и для вариадиков нам нужно писать некоторый "дополнительный" код. Например, когда мы хотим написать функцию sum, которая складывает все аргументы, которые ей передали, рекурсивно. Мы должны определить базу для рекурсии. Выглядит это так:
Если бы у нас не было первого определения, то рекурсия дошла бы до нуля аргументов и не смогла бы инстанцировать функцию без аргументов и компиляция бы провалилась.
Но важно еще кое-что заметить. Что мы так или иначе предполагаем, что все наши аргументы могут успешно быть сложены с интом. Это довольно сильное ограничение, потому что может я хочу и матрицы складывать тоже этой функцией. А тут такого сделать не получится.
Но решение этих проблем есть!
Называется fold expression. Появилось это спасение в С++17 и позволяет писать намного более простой код. Посмотрим, как будет выглядеть прошлый пример при его использовании.
Никаких дополнительных определений и мы можем хоть обезьянок складывать, хоть их испражнения(и все в одной функции).
Однако есть все-таки одно ограничение. Функцию Sum не получится инстанцировать без аргументов. Это свойство оператора сложения. И об этом в том числе мы поговорим завтра, когда будем подробнее разбирать внутрянку fold expression.
Make things simplier. Stay cool.
#cpp11 #cpp17 #template
Стандарт C++11 привнес в нашу жизнь замечательную фичу - variadic templates, которая является очень мощным инструментом в метапрограммировании. Она используется, когда нам необходимо написать функцию, которая принимает неопределенное количество аргументов. Ранее такой возможности в С++ не было (имею ввиду типобезопасные шаблонные функции) и приходилось отдельно специфицировать функцию в начале с одним аргументом, потом с двумя, потом с тремя и так далее, пока не надоест, не настанет обед или больше не нужно будет. Не очень удобненько.
Однако и для вариадиков нам нужно писать некоторый "дополнительный" код. Например, когда мы хотим написать функцию sum, которая складывает все аргументы, которые ей передали, рекурсивно. Мы должны определить базу для рекурсии. Выглядит это так:
auto SumCpp11() {
return 0;
}
template<typename T1, typename... T>
auto SumCpp11(T1 s, T... ts) {
return s + SumCpp11(ts...);
}Если бы у нас не было первого определения, то рекурсия дошла бы до нуля аргументов и не смогла бы инстанцировать функцию без аргументов и компиляция бы провалилась.
Но важно еще кое-что заметить. Что мы так или иначе предполагаем, что все наши аргументы могут успешно быть сложены с интом. Это довольно сильное ограничение, потому что может я хочу и матрицы складывать тоже этой функцией. А тут такого сделать не получится.
Но решение этих проблем есть!
Называется fold expression. Появилось это спасение в С++17 и позволяет писать намного более простой код. Посмотрим, как будет выглядеть прошлый пример при его использовании.
template<typename ...Args>
auto SumCpp17(Args ...args) {
return (args + ...);
}
Никаких дополнительных определений и мы можем хоть обезьянок складывать, хоть их испражнения(и все в одной функции).
Однако есть все-таки одно ограничение. Функцию Sum не получится инстанцировать без аргументов. Это свойство оператора сложения. И об этом в том числе мы поговорим завтра, когда будем подробнее разбирать внутрянку fold expression.
Make things simplier. Stay cool.
#cpp11 #cpp17 #template
👍18🔥9❤3
Fold expression. Подробности.
В сущности, fold expression - сворачивание всего пака шаблонных параметров с помощью комбинации синтаксиса variadic templates и бинарных операторов. Есть всего 4 формата, в которых можно использовать эту фичу.
1️⃣ ( pack op ...) - унарный правый фолд
2️⃣ ( ... op pack) - унарный левый фолд
3️⃣ (pack op ... op init ) - бинарный правый фолд
4️⃣ (init op ... op pack) - бинарный левый фолд
где pack - выражение, содержащее нераспакованный набор шаблонных параметров. op - бинарный оператор. В последних двух случаях он должен быть одинаковым справа и слева от точек. В число бинарных операторов входит почти все, что вы могли бы себе представить: + - / % ^ & | = < > << >> += -= = /= %= ^= &= |= <<= >>= == != <= >= && || , . ->. init - выражение, которое никак не относится к шаблонным параметрам и является базой вычислений. Это как в std::accumulate вы можете выставить начальное значение для аггрегации. Вот это тоже самое.
Опустил некоторые не очень важные детали. Дай бог вам попользоваться этой фичей, в корнер кейсах разберетесь сами.
Очень важное уточнение по поводу левых и правых фолдов.
👉🏿 Унарный правый фолд (E op ...) раскрывается в (E1 op (... op (En-1 op En)))
👉🏿 Унарный левый фолд (... op E) раскрывается в (((E1 op E2) op ...) op En)
👉🏿 Бинарный правый фолд (E op ... op init) раскрывается в (E1 op (... op (En−1 op (EN op init))))
👉🏿 Бинарный левый фолд(init op ... op E) раскрывается в ((((init op E1) op E2) op ...) op En)
И тут очень сильно решает ассициативность операции. То есть независимость от порядка постановки скобое. Если оператор обладает этим свойством, как например сложение или умножение, то можете не париться о порядке. Пишите, как удобно. А вот если от порядка операндов зависит итоговый результат операции(тот же бинарный сдвиг), то подумайте, какой именно фолд подойдет для решения вашей задачи.
А помните, я вчера упоминал функцию Sum, которую нельзя расшаблонивать с нулевым количеством аргументов(там в комментах @PyXiion придумал как, но я сейчас имею ввиду нативный формат без оберток)? Вот сейчас и коснемся этого вопроса.
Функция без аргументов - всегда был особым случаем при использовании вариадик шаблонов. И для fold expression она также является таковым. Тут следующие правила:
💥 У оператора Логическое И (&&) значение для пустого набора параметров - true.
💥 У оператора Логическое ИЛИ (||) значение для пустого набора параметров - false.
💥 У оператора "запятая" (,) значение для пустого набора параметров - void().
☠️ Для всех остальных операторов конкретизация шаблона с пустым набором параметров запрещена.
Почему так? А непонятно, какой результат у сложения ничего с ничем. Или непонятно, что будет если у ничего сдвинуть несуществующие биты вправо на никакое количество позиций. И так далее. И соответственно, тут надо либо использовать инициализатор, либо обвязки писать. Но вот для трех операторов нашелся логичный результат, поэтому они вот такие особенные.
В следующий раз в подробностях разберем, как нормально принтоваться с помощью fold expression. Это довольно популярное и нужное применение. Может и не в продакшен коде. Но при экспериментах или при отладке поможет сильно сократить время и ошибки.
Постарался кое-где использовать ваши синонимы, отпишите, как звучит.
Explore internals of things. Stay cool.
#cpp17 #template
В сущности, fold expression - сворачивание всего пака шаблонных параметров с помощью комбинации синтаксиса variadic templates и бинарных операторов. Есть всего 4 формата, в которых можно использовать эту фичу.
1️⃣ ( pack op ...) - унарный правый фолд
2️⃣ ( ... op pack) - унарный левый фолд
3️⃣ (pack op ... op init ) - бинарный правый фолд
4️⃣ (init op ... op pack) - бинарный левый фолд
где pack - выражение, содержащее нераспакованный набор шаблонных параметров. op - бинарный оператор. В последних двух случаях он должен быть одинаковым справа и слева от точек. В число бинарных операторов входит почти все, что вы могли бы себе представить: + - / % ^ & | = < > << >> += -= = /= %= ^= &= |= <<= >>= == != <= >= && || , . ->. init - выражение, которое никак не относится к шаблонным параметрам и является базой вычислений. Это как в std::accumulate вы можете выставить начальное значение для аггрегации. Вот это тоже самое.
Опустил некоторые не очень важные детали. Дай бог вам попользоваться этой фичей, в корнер кейсах разберетесь сами.
Очень важное уточнение по поводу левых и правых фолдов.
👉🏿 Унарный правый фолд (E op ...) раскрывается в (E1 op (... op (En-1 op En)))
👉🏿 Унарный левый фолд (... op E) раскрывается в (((E1 op E2) op ...) op En)
👉🏿 Бинарный правый фолд (E op ... op init) раскрывается в (E1 op (... op (En−1 op (EN op init))))
👉🏿 Бинарный левый фолд(init op ... op E) раскрывается в ((((init op E1) op E2) op ...) op En)
И тут очень сильно решает ассициативность операции. То есть независимость от порядка постановки скобое. Если оператор обладает этим свойством, как например сложение или умножение, то можете не париться о порядке. Пишите, как удобно. А вот если от порядка операндов зависит итоговый результат операции(тот же бинарный сдвиг), то подумайте, какой именно фолд подойдет для решения вашей задачи.
А помните, я вчера упоминал функцию Sum, которую нельзя расшаблонивать с нулевым количеством аргументов(там в комментах @PyXiion придумал как, но я сейчас имею ввиду нативный формат без оберток)? Вот сейчас и коснемся этого вопроса.
Функция без аргументов - всегда был особым случаем при использовании вариадик шаблонов. И для fold expression она также является таковым. Тут следующие правила:
💥 У оператора Логическое И (&&) значение для пустого набора параметров - true.
💥 У оператора Логическое ИЛИ (||) значение для пустого набора параметров - false.
💥 У оператора "запятая" (,) значение для пустого набора параметров - void().
☠️ Для всех остальных операторов конкретизация шаблона с пустым набором параметров запрещена.
Почему так? А непонятно, какой результат у сложения ничего с ничем. Или непонятно, что будет если у ничего сдвинуть несуществующие биты вправо на никакое количество позиций. И так далее. И соответственно, тут надо либо использовать инициализатор, либо обвязки писать. Но вот для трех операторов нашелся логичный результат, поэтому они вот такие особенные.
В следующий раз в подробностях разберем, как нормально принтоваться с помощью fold expression. Это довольно популярное и нужное применение. Может и не в продакшен коде. Но при экспериментах или при отладке поможет сильно сократить время и ошибки.
Постарался кое-где использовать ваши синонимы, отпишите, как звучит.
Explore internals of things. Stay cool.
#cpp17 #template
🔥11👍4❤3😁3🥴1
Принтуем с fold expression
Для начала разберем, как бы все выглядело до С++17.
Задача - вывести на экран все аргументы функции подряд. Не так уж и сложно. Код будет выглядеть примерно вот так:
Это будет работать, но не очень прикольно выводить аргументы прям подряд символами. Нужно какое-то форматирование. Например, между аргументами выводить пробел, а в конце перенести строку. Тут уже все несколько усложняется...
Нам мало того, что пришлось использовать базу рекурсии, так еще и прокси-функцию, которая допиливает форматирование. Слишко МНОГА БУКАВ. Ща исправим.
Вот так будет выглядеть базовый принт без форматирования на fold expression:
Уже лучше. Точнее не так. Проще не бывает уже)
Как видите, здесь я использую бинарный левый фолд. В качестве инициализатора выступает стандартный поток вывода и он слева не только потому, что так обычно принято, а потому что оператор << также применяется например для бинарного сдвига. И чтобы мы всегда именно в поток писали, нужно, чтобы слева всегда был нужный поток. Тогда будет вызываться соответствующая перегрузка для ostream'ов и каждый раз будет возвращаться ссылка на этот поток. Таким образом мы и будем продолжать писать именно в него.
Но как тут быть с форматингом? args тут просто раскроются в последовательность "arg1 << arg2 << arg3" и тд
И непонятно, как в таких условиях добавить вывод пробела, не придумывая нагромождения в виде проксей и прочего. Для решения этой проблемы надо воспользоваться двумя хаками:
1️⃣ Не обязательно использовать сырой пакет параметров. Можно использовать функцию, принимающую этот пак.
2️⃣ Применяя оператор запятую, мы можем в операндах выполнять любое выражение, даже возвращающее void.
Получается такая штука:
Здесь мы за счет лямбды и запятой выполняем каждый раз отдельную операцию вывода в поток с пробелом. А затем вместо init выражения подставляем вывод конца строки.
Прикольный хак, я считаю! В нем много нюансов, но его явно можно взять себе на вооружение и использовать в специфичных кейсах.
Hack this life. Stay cool.
#cpp17 #template
Для начала разберем, как бы все выглядело до С++17.
Задача - вывести на экран все аргументы функции подряд. Не так уж и сложно. Код будет выглядеть примерно вот так:
void print() {
}
template<typename T1, typename... T>
void print(T1 s, T... ts) {
std::cout << s;
print(ts...);
}
print(1.7, -2, "qwe");
// Output: 1.7-2qweЭто будет работать, но не очень прикольно выводить аргументы прям подряд символами. Нужно какое-то форматирование. Например, между аргументами выводить пробел, а в конце перенести строку. Тут уже все несколько усложняется...
void print_impl() {
}
template<typename T1, typename... T>
void print_impl(T1 s, T... ts) {
std::cout << ' ' << s;
print_impl(ts...);
}
template<typename T1, typename... T>
void print(T1 s, T... ts) {
std::cout << s;
print_impl(ts...);
std::cout << std::endl;
}
print(1.7, -2, "qwe");
print("You", ", our subscribers,", "are", "the", "best!!");
// Output:
// 1.7 -2 qwe
// You , our subscribers, are the best!!Нам мало того, что пришлось использовать базу рекурсии, так еще и прокси-функцию, которая допиливает форматирование. Слишко МНОГА БУКАВ. Ща исправим.
Вот так будет выглядеть базовый принт без форматирования на fold expression:
template<typename... Args>
void print(Args&&... args) {
(std::cout << ... << args);
}
print(1.7, -2, "qwe");
// Output: 1.7-2qwe
Уже лучше. Точнее не так. Проще не бывает уже)
Как видите, здесь я использую бинарный левый фолд. В качестве инициализатора выступает стандартный поток вывода и он слева не только потому, что так обычно принято, а потому что оператор << также применяется например для бинарного сдвига. И чтобы мы всегда именно в поток писали, нужно, чтобы слева всегда был нужный поток. Тогда будет вызываться соответствующая перегрузка для ostream'ов и каждый раз будет возвращаться ссылка на этот поток. Таким образом мы и будем продолжать писать именно в него.
Но как тут быть с форматингом? args тут просто раскроются в последовательность "arg1 << arg2 << arg3" и тд
И непонятно, как в таких условиях добавить вывод пробела, не придумывая нагромождения в виде проксей и прочего. Для решения этой проблемы надо воспользоваться двумя хаками:
1️⃣ Не обязательно использовать сырой пакет параметров. Можно использовать функцию, принимающую этот пак.
2️⃣ Применяя оператор запятую, мы можем в операндах выполнять любое выражение, даже возвращающее void.
Получается такая штука:
template<typename ...Args>
void print(Args&&... args) {
auto print_with_space = [](const auto& v) { std::cout << v << ' '; };
(print_with_space(args), ... , (std::cout << std::endl));
}
print(1.7, -2, "qwe");
print("You, "are", "the", "best!!");
// Output:
// 1.7 -2 qwe
// You are the best!!
Здесь мы за счет лямбды и запятой выполняем каждый раз отдельную операцию вывода в поток с пробелом. А затем вместо init выражения подставляем вывод конца строки.
Прикольный хак, я считаю! В нем много нюансов, но его явно можно взять себе на вооружение и использовать в специфичных кейсах.
Hack this life. Stay cool.
#cpp17 #template
🔥10👍7❤5
Что выведется на экран?
Попробуем новую рубрику на канале - #quiz. Мы задаем вопрос - а вы выбираете один из предоставленных ответов. Все обсуждения в комментах. А вечером выходит пост с подробными объяснениями. Погнали!
Допустим, я хочу сдвинуть 4-х байтное знаковое число на 31 бит вправо и вывести значение получившегося числа.
Для определенности предположим, что number = -12.
Знаю, знаю. Я не совсем больной ублюдок, чтобы заставлять вас отрицательные числа в бинарный формат хранения переводить.
Считайте, что -12 представляется в памяти, как 1111 1111 1111 1111 1111 1111 1111 0100. Почти наверняка так и будет.
(Опрос следующим постом выйдет)
Попробуем новую рубрику на канале - #quiz. Мы задаем вопрос - а вы выбираете один из предоставленных ответов. Все обсуждения в комментах. А вечером выходит пост с подробными объяснениями. Погнали!
Допустим, я хочу сдвинуть 4-х байтное знаковое число на 31 бит вправо и вывести значение получившегося числа.
int number = -12;
int result = number >> 31;
std::cout << result << std::endl;
Для определенности предположим, что number = -12.
Знаю, знаю. Я не совсем больной ублюдок, чтобы заставлять вас отрицательные числа в бинарный формат хранения переводить.
Считайте, что -12 представляется в памяти, как 1111 1111 1111 1111 1111 1111 1111 0100. Почти наверняка так и будет.
(Опрос следующим постом выйдет)
🔥8👍4❤2
Что выведется в консоль в этом случае?
Anonymous Poll
39%
1
10%
0
22%
-1
10%
2^32 - 1
14%
Мне-то откуда знать???
4%
Hello, World!!!
🔥6❤2👍2
Ответ на вопрос выше не так уж и прост на самом деле. Начнем с того, что когда вы запустите этот код на своей машине, то получите ответ: -1.
Эм. Неожиданно! "Как так получается?" - спросите вы меня. "Ведь я же знаю, как работает бинарный сдвиг: берем да и сдвигаем биты вправо и позади оставляем нули. В итоге получатся все нолики и самый младший бит 1. А это 1, а не -1!".
Логика железная и с ней не поспоришь. Однако в этой цепочке есть одно неверное утверждение. На самом деле вы не знаете, как работает правый бинарный сдвиг!(ну те, кто так рассуждают)
Заинтригованы?
Тогда мы идем к вам!
Что было до С++20.
Рассуждения верные только для беззнаковых чисел. Для знаковых все определяется конкретной реализацией. А у нас как раз такой вариант. Так еще все от стандарта зависит. Так что правильный ответ: "Мне-то откуда знать???". В общем случае вы вряд ли знаете, как во всех компиляторах это реализовано, а спрашивал я без привязки к конкретной реализации и стандарту. Да, вот так завуалировал ответ. Имею право.
Почему я тогда утверждаю, что вы на своих машинах получите -1?
Потому что в большинстве реализаций правый битовый сдвиг для знаковых чисел работает с помощью так называемого "арифметического сдвига".
Что это за акула такая.
Когда вы делаете правый сдвиг для беззнаковых чисел, то просто старшие разряды заполняете нулями. Арифметический же сдвиг заполняет старшие разряды не нулями, а знаковым битом. Таким образом, правый сдвиг любого 4-х байтного знакового числа оставит после себя либо 32 бита нулей (в случае положительного числа), либо 32 бита единичек(в случае отрицательного числа). А все единички в битах - это -1 для знаковых чисел.
С++20 начинает нам гарантировать, что правый сдвиг для знаковых чисел выполняется с помощью арифметического сдвига. Спасибо Дмитрию за это уточнение)
Вот такой прикол. Надеюсь, что я многих удивил тем, как это работает)
Арифметический сдвиг - хоть теперь и стандартное поведение, но
Во-первых, не все разрабы имеют достаточного опыта на 20-х плюсах, чтобы понять, что это стандартное поведение
Во-вторых, в каких-то проектах(которых на самом деле огромное количество) до сих пор не используется этот стандарт
В-третьих, слишком мало времени прошло с момента стандартизации. Если раньше это было грязным нестандартным приемом, то в умах людей он таковым еще долго останется. И это не изменится взмахом палочки комитета.
Поэтому не стоит использовать такие приколы без острой необходимости и правильного, подробного комментирования в случае этой необходимости.
Stay surprised. Stay cool.
#cpp20 #compiler
Эм. Неожиданно! "Как так получается?" - спросите вы меня. "Ведь я же знаю, как работает бинарный сдвиг: берем да и сдвигаем биты вправо и позади оставляем нули. В итоге получатся все нолики и самый младший бит 1. А это 1, а не -1!".
Логика железная и с ней не поспоришь. Однако в этой цепочке есть одно неверное утверждение. На самом деле вы не знаете, как работает правый бинарный сдвиг!(ну те, кто так рассуждают)
Заинтригованы?
Тогда мы идем к вам!
Что было до С++20.
Рассуждения верные только для беззнаковых чисел. Для знаковых все определяется конкретной реализацией. А у нас как раз такой вариант. Так еще все от стандарта зависит. Так что правильный ответ: "Мне-то откуда знать???". В общем случае вы вряд ли знаете, как во всех компиляторах это реализовано, а спрашивал я без привязки к конкретной реализации и стандарту. Да, вот так завуалировал ответ. Имею право.
Почему я тогда утверждаю, что вы на своих машинах получите -1?
Потому что в большинстве реализаций правый битовый сдвиг для знаковых чисел работает с помощью так называемого "арифметического сдвига".
Что это за акула такая.
Когда вы делаете правый сдвиг для беззнаковых чисел, то просто старшие разряды заполняете нулями. Арифметический же сдвиг заполняет старшие разряды не нулями, а знаковым битом. Таким образом, правый сдвиг любого 4-х байтного знакового числа оставит после себя либо 32 бита нулей (в случае положительного числа), либо 32 бита единичек(в случае отрицательного числа). А все единички в битах - это -1 для знаковых чисел.
С++20 начинает нам гарантировать, что правый сдвиг для знаковых чисел выполняется с помощью арифметического сдвига. Спасибо Дмитрию за это уточнение)
Вот такой прикол. Надеюсь, что я многих удивил тем, как это работает)
Арифметический сдвиг - хоть теперь и стандартное поведение, но
Во-первых, не все разрабы имеют достаточного опыта на 20-х плюсах, чтобы понять, что это стандартное поведение
Во-вторых, в каких-то проектах(которых на самом деле огромное количество) до сих пор не используется этот стандарт
В-третьих, слишком мало времени прошло с момента стандартизации. Если раньше это было грязным нестандартным приемом, то в умах людей он таковым еще долго останется. И это не изменится взмахом палочки комитета.
Поэтому не стоит использовать такие приколы без острой необходимости и правильного, подробного комментирования в случае этой необходимости.
Stay surprised. Stay cool.
#cpp20 #compiler
🔥21❤6👍6