Что нужно возвращать из оператора присваивания?
Пост скорее для новичков, но менее интересным он от этого не становится. В основном все пишут, как принято. Но естественно, у каждой самой маленькой детали под собой есть основание.
Коротенький рекап. Операторы присваивания нужны для перенятия свойств других объектов. В общем случае даже объектов других классов. Но нас интересуют 2 особенных члена классов - копирующий и перемещающий операторы присваивания. Первый нужен, что скопировать содержимое одного объекта в другой и по договоренности обычно принимает константную lvalue ссылку на объект того же класса. Второй нужен для перемещения ресурсов из одного объекта в другой и принимает rvalue reference на объект того же класса.
Ну вот скопировали или переместили мы ресурсы. Как и любая функция, этот оператор должен что-то возвращать. И тут есть несколько вариантов: ничего не возвращать(void), возвращать в каком-то виде объект вообще другого класса, возвращать объект по значению, по неконстантной ссылке, по константной ссылке и по rvalue ссылке. Довольно много вариантов, поэтому будем отметать их в порядке очевидной непригодности.
Во всем посте дальше я буду писать про копирующий оператор, чтобы не распыляться, но несложно будет перейти к логике перемещающего оператора. Но в начале разберемся с семантикой. Я, как пользователь класса, ожидаю, что объект из которого я копирую не изменится, после выполнения оператора два объекта будут семантически идентичны, и чтобы я с объектом не сделал внутри одной строчки, он не потеряет свое новоприобретенное значение(если я явно не вызову std::move). С этим определились, поехали разбирать кейсы.
1️⃣ Можем вернуть объект вообще другого класса. Семантика такого решения будет самой неочевидной для пользователей класса. И хоть я могу представить что-то подобное для других операторов, например, если вычесть друг из друга 2 даты, то получим какое-то число, а не дату. Но для оператора присваивания я даже кейса никакого не могу привести, поэтому сразу скипаем этот вариант.
2️⃣ Можем вернуть объект по rvalue reference. В этом случае я могу потерять новоприобретенные ресурсы, если передам результат оператора в функцию, которая принимает аргумент по значению или по rvalue reference.
Это нарушает ожидаемую семантику поведения копирующего оператора, поэтому тоже не подходит.
3️⃣ Можем вернуть объект по значению. Основной и решающий, на мой взгляд, недостаток - это снижение производительности на лишнее копирование объекта и его удаление.
В итоге мы видим 2 лишних копирования и 2 лишних вызова деструктора. Этого достаточно, чтобы отбросить вариант. Лишних я говорю, потому что знаю просто, что их можно избежать. Просто даже по виду выражения c = b = a; видно, что тут просто 2 присваивания. И я не ожидаю, что вылезет еще какое-то копирование. Если неосознанно пользоваться таким оператором, то можно снижать перфоманс приложения, даже не осознавая этого.
Остальные пункты в один пост не влезет. Придется выделить во вторую часть.
Stay conscious about small things. Stay cool.
#cppcore
Пост скорее для новичков, но менее интересным он от этого не становится. В основном все пишут, как принято. Но естественно, у каждой самой маленькой детали под собой есть основание.
Коротенький рекап. Операторы присваивания нужны для перенятия свойств других объектов. В общем случае даже объектов других классов. Но нас интересуют 2 особенных члена классов - копирующий и перемещающий операторы присваивания. Первый нужен, что скопировать содержимое одного объекта в другой и по договоренности обычно принимает константную lvalue ссылку на объект того же класса. Второй нужен для перемещения ресурсов из одного объекта в другой и принимает rvalue reference на объект того же класса.
Ну вот скопировали или переместили мы ресурсы. Как и любая функция, этот оператор должен что-то возвращать. И тут есть несколько вариантов: ничего не возвращать(void), возвращать в каком-то виде объект вообще другого класса, возвращать объект по значению, по неконстантной ссылке, по константной ссылке и по rvalue ссылке. Довольно много вариантов, поэтому будем отметать их в порядке очевидной непригодности.
Во всем посте дальше я буду писать про копирующий оператор, чтобы не распыляться, но несложно будет перейти к логике перемещающего оператора. Но в начале разберемся с семантикой. Я, как пользователь класса, ожидаю, что объект из которого я копирую не изменится, после выполнения оператора два объекта будут семантически идентичны, и чтобы я с объектом не сделал внутри одной строчки, он не потеряет свое новоприобретенное значение(если я явно не вызову std::move). С этим определились, поехали разбирать кейсы.
1️⃣ Можем вернуть объект вообще другого класса. Семантика такого решения будет самой неочевидной для пользователей класса. И хоть я могу представить что-то подобное для других операторов, например, если вычесть друг из друга 2 даты, то получим какое-то число, а не дату. Но для оператора присваивания я даже кейса никакого не могу привести, поэтому сразу скипаем этот вариант.
2️⃣ Можем вернуть объект по rvalue reference. В этом случае я могу потерять новоприобретенные ресурсы, если передам результат оператора в функцию, которая принимает аргумент по значению или по rvalue reference.
struct Class {
Class(int num) : a{num} {std::cout << "Ctor" << "n";}
Class&& operator=(const B& other) { a = other.a; return std::move(*this);}
Class(Class&& other) {a = other.a; other.a = 0;}
int a;
};
void func(Class obj) {
std::cout << obj.a << "n";
}
int main() {
Class a{2}, b{3};
func(b = a);
std::cout << b.a << "n";
}
//OUTPUT:
Ctor
Ctor
2
0
Это нарушает ожидаемую семантику поведения копирующего оператора, поэтому тоже не подходит.
3️⃣ Можем вернуть объект по значению. Основной и решающий, на мой взгляд, недостаток - это снижение производительности на лишнее копирование объекта и его удаление.
struct Class {
Class(int num) : a{num} {std::cout << "Ctor" << "n";}
Class(const Class& other) {a = other.a; std::cout << "Copy Ctor" << "n";}
Class operator=(const Class& other) { a = other.a; std::cout << "Copy Assign" << "n"; return *this;}
Class(Class&& other) {a = other.a; other.a = 0;}
~Class() {std::cout << "Dtor" << "n";}
int a;
};
int main() {
Class a{2}, b{3}, c{4};
c = b = a;
std::cout << a.a << " " << b.a << " " << c.a << "n";
}
//OUTPUT:
Ctor
Ctor
Ctor
Copy Assign
Copy Ctor
Copy Assign
Copy Ctor
Dtor
Dtor
2 2 2
Dtor
Dtor
Dtor
В итоге мы видим 2 лишних копирования и 2 лишних вызова деструктора. Этого достаточно, чтобы отбросить вариант. Лишних я говорю, потому что знаю просто, что их можно избежать. Просто даже по виду выражения c = b = a; видно, что тут просто 2 присваивания. И я не ожидаю, что вылезет еще какое-то копирование. Если неосознанно пользоваться таким оператором, то можно снижать перфоманс приложения, даже не осознавая этого.
Остальные пункты в один пост не влезет. Придется выделить во вторую часть.
Stay conscious about small things. Stay cool.
#cppcore
Что нужно возвращать из оператора присваивания? Ч2
В прошлом посте мы поговорили о том, что точно не нужно возвращать из оператора присваивания, потому что это нарушает его семантику. У нас остались для рассмотрения 3 варианта: void, неконстантная ссылка и константная ссылка. Их всех корректно использовать, только какие-то будут накладывать определенные ограничения.
Самый банальный пример - цепочка присваивания. Это конструкция вида: a = b = c;. Если честно, то никогда такой штукой не пользовался. Однако надо учитывать, что кому-то могут понадобиться такие конструкции. Очевидно, что если оператор присваивания будет возвращать void, цепочкой присваиваний нельзя будет пользоваться для такого класса.
Ну или может быть вы хотите присвоить значение объекту и потом для конкретно этого объекта вызвать метод. Например так:
Если метод вернет void, то такая штука просто не скомпилируется. Это кстати довольно удобно делать в условиях. Типа того:
Мы хотим использовать результат SomeFunc дальше по коду, поэтому хотим присвоить его именованной переменной. Но и хотим сразу проверить, валидный ли объект. Эти операции удобно делать сразу налету в условии. А если оператор вернет void, то этим удобным инструментом нельзя будет пользоваться. Конечно, этот пример скорее для перемещающего присваивания, но суть та же.
Теперь осталось разобраться с константностью. На самом деле даже далеко уходить не надо. Возьмем кейс с условием. В случае константной ссылки мы не сможем выполнять неконстантные методы, которые собственно и нельзя вызывать у константных ссылок. Потому что подразумевается, что константная ссылка - read-only сущность и нам запрещено через нее изменять объект, на который указывает ссылка. Это несколько ограничивает наши возможности.
Ну и остается только неконстантная ссылка. У нее нет side эффектов семантически и она позволяет выполнять все операции, которые нам были недоступны выше.
Однако помимо всего прочего есть еще одна причина делать возвращаемое значение неконстантной ссылкой. Такую форму и семантику реализуют операторы присваивания для тривиальных типов. Все мы в нашем плюсовом юношестве наигрались с интами и флотами и просто на интуитивном уровне понимаем, как для них все работает. И очень круто, когда пользовательский класс перенимает такое же поведение. Потому что пользователю будет намного проще работать с таким классом. Меньше времени на понимание работы с объектом и памяти на сохранение информации о нем - больше производительность программиста. Это конвертируется в денежку. Всем это выгодно.
Поэтому без особой надобности не изменяйте этот де-факто стандартный вид возвращаемого значения. Вы сможете юзать весь объем возможностей использования класса и сделаете его проще для понимания.
Use all of your abilities. Stay cool.
#cppcore
В прошлом посте мы поговорили о том, что точно не нужно возвращать из оператора присваивания, потому что это нарушает его семантику. У нас остались для рассмотрения 3 варианта: void, неконстантная ссылка и константная ссылка. Их всех корректно использовать, только какие-то будут накладывать определенные ограничения.
Самый банальный пример - цепочка присваивания. Это конструкция вида: a = b = c;. Если честно, то никогда такой штукой не пользовался. Однако надо учитывать, что кому-то могут понадобиться такие конструкции. Очевидно, что если оператор присваивания будет возвращать void, цепочкой присваиваний нельзя будет пользоваться для такого класса.
Ну или может быть вы хотите присвоить значение объекту и потом для конкретно этого объекта вызвать метод. Например так:
(obj = temp_obj).foo();
Если метод вернет void, то такая штука просто не скомпилируется. Это кстати довольно удобно делать в условиях. Типа того:
if ((obj = SomeFunc()).isValid()) {...}
Мы хотим использовать результат SomeFunc дальше по коду, поэтому хотим присвоить его именованной переменной. Но и хотим сразу проверить, валидный ли объект. Эти операции удобно делать сразу налету в условии. А если оператор вернет void, то этим удобным инструментом нельзя будет пользоваться. Конечно, этот пример скорее для перемещающего присваивания, но суть та же.
Теперь осталось разобраться с константностью. На самом деле даже далеко уходить не надо. Возьмем кейс с условием. В случае константной ссылки мы не сможем выполнять неконстантные методы, которые собственно и нельзя вызывать у константных ссылок. Потому что подразумевается, что константная ссылка - read-only сущность и нам запрещено через нее изменять объект, на который указывает ссылка. Это несколько ограничивает наши возможности.
Ну и остается только неконстантная ссылка. У нее нет side эффектов семантически и она позволяет выполнять все операции, которые нам были недоступны выше.
Однако помимо всего прочего есть еще одна причина делать возвращаемое значение неконстантной ссылкой. Такую форму и семантику реализуют операторы присваивания для тривиальных типов. Все мы в нашем плюсовом юношестве наигрались с интами и флотами и просто на интуитивном уровне понимаем, как для них все работает. И очень круто, когда пользовательский класс перенимает такое же поведение. Потому что пользователю будет намного проще работать с таким классом. Меньше времени на понимание работы с объектом и памяти на сохранение информации о нем - больше производительность программиста. Это конвертируется в денежку. Всем это выгодно.
Поэтому без особой надобности не изменяйте этот де-факто стандартный вид возвращаемого значения. Вы сможете юзать весь объем возможностей использования класса и сделаете его проще для понимания.
Use all of your abilities. Stay cool.
#cppcore
Просто генерируем числа
Обычно каждый раз, когда хочется просто сгенерировать число, приходится танцевать с бубном. Ну ладно не просто. А в плюсовом стиле. Для сишечки есть прекрасный генератор rand(). НО! Им неудобно пользоваться, если нужно число из определенного ренджа. И для нормальной рандомизации нужно еще писать srand(time(0)). И это ладно еще, можно запомнить(хотя у меня никогда не получалось). Но вот это
Простите, перебор. Может у кого-то и хватает мощностей мозга спокойно воспроизвести эту конструкцию, чтобы просто С@КА СГЕНЕРИРОВАТЬ ОДНО ЧИСЛО. Но у меня нет. Приходится каждый раз лезть в гугл в поисках того самого рабочего примера. Ставьте лайк, если мы с вами похожи.
Однако есть свет в конце тоннеля! Свет называется std::experimental::randint. Это шаблонная функция, которая принимает рендж чисел от и до. Важное уточнение, что uint8_t и int8_t не подходят для использования этим генератором. Ну и если первое число больше второго - поведение неопределено. Но это все неважно, потому что с этой функцией генерация целого числа происходит вот так:
ВСЁ. Это вот настолько просто, насколько и должно быть. Естественно, под капотом там тот же тред-локал инстанс std::uniform_int_distribution.
Эта фича дергается из <experimental/random> и доступна с 17 версии плюсов. Поэтому большинству из нас она будет доступна.
Cтоит также понимать все недостатки экспериментальных библиотек и не полагаться на них в критически важных местах. Не зря их назвали экспериментальными.
Однако приятно, что разработчики стандартной библиотеки учитывают боль своих пользователей и работают над удобством использования ее инструментов.
Make the world comfortable. Stay cool.
Обычно каждый раз, когда хочется просто сгенерировать число, приходится танцевать с бубном. Ну ладно не просто. А в плюсовом стиле. Для сишечки есть прекрасный генератор rand(). НО! Им неудобно пользоваться, если нужно число из определенного ренджа. И для нормальной рандомизации нужно еще писать srand(time(0)). И это ладно еще, можно запомнить(хотя у меня никогда не получалось). Но вот это
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution distrib(1, 6);
auto number = distrib(gen);
Простите, перебор. Может у кого-то и хватает мощностей мозга спокойно воспроизвести эту конструкцию, чтобы просто С@КА СГЕНЕРИРОВАТЬ ОДНО ЧИСЛО. Но у меня нет. Приходится каждый раз лезть в гугл в поисках того самого рабочего примера. Ставьте лайк, если мы с вами похожи.
Однако есть свет в конце тоннеля! Свет называется std::experimental::randint. Это шаблонная функция, которая принимает рендж чисел от и до. Важное уточнение, что uint8_t и int8_t не подходят для использования этим генератором. Ну и если первое число больше второго - поведение неопределено. Но это все неважно, потому что с этой функцией генерация целого числа происходит вот так:
int random_number = std::experimental::randint(100, 999);
ВСЁ. Это вот настолько просто, насколько и должно быть. Естественно, под капотом там тот же тред-локал инстанс std::uniform_int_distribution.
Эта фича дергается из <experimental/random> и доступна с 17 версии плюсов. Поэтому большинству из нас она будет доступна.
Cтоит также понимать все недостатки экспериментальных библиотек и не полагаться на них в критически важных местах. Не зря их назвали экспериментальными.
Однако приятно, что разработчики стандартной библиотеки учитывают боль своих пользователей и работают над удобством использования ее инструментов.
Make the world comfortable. Stay cool.
Варианты запуска потоков
В прошлом я рассказал, что можно запускать поток не только свободной функцией, но и методом какого-то объекта. Во время публикации этого поста у меня появилось ощущение, что со стороны не совсем понятно, как компилятор различает аргументы функции, саму функцию и объект, из метода которого потенциально поток может быть запущен. Поэтому сегодняшний пост по это.
В С++ есть такое понятие - callable objects. Это просто сущности, которые можно вызвать. Это функции, лямбды, функторы, методы и так далее. Как можно единообразно вызывать все эти сущности? Сходу в голову ничего не приходит, но в стандартной библиотеке начиная с С++17 есть такая функция std::invoke. Вот ее сигнатура:
template< class F, class... Args>
std::invokeresultt<F, Args...> invoke(F&& f, Args&&... args) noexcept
И существуют правила, по которым эта функция парсит свои аргументы. Но не только она, а все сущности, которые как бы "вовлекают" в работу callable объект. Открывайте окна, потому что щас немного духоты будет.
Пусть INVOKE(f, t1, t2, ..., tN) - выражение, обозначающее вовлечение в работу функционального объекта f. Тогда:
Если f - указатель на метод класса T:
t1 - объект типа Т, или ссылка на объект типа Т,
или ссылка на наследника типа Т, то этот INVOKE превращается в (t1.*f)(t2, ..., tN).
t1 - ни что из перечисленного выше(то есть указатель на объект), то INVOKE превращается в ((*t1).*f)(t2, ..., tN).
Если N=1 и f - указатель на член класса:
t1 - объект типа Т, или ссылка на объект типа Т,
или ссылка на наследника типа Т, то этот INVOKE превращается в t1.*f.
t1 - ни что из перечисленного выше(то есть указатель на объект), то INVOKE превращается в (*t1).*f.
Во всех других случая INVOKE разворачивается в обычный вызов функции f(t1, t2, ..., tN).
То есть конструктор std::thread, который внутри себя дергает std::invoke, на основе вот этих простых правил и решает, что ему на самом деле передали и как себя дальше вести.
Пройдитесь глазами по схеме выбора еще раз, чтобы лучше уложить ее себе в голову, потому что она не прям простая. Тут кстати есть отсылочка к моему давнему посту про вызов метода объекта через указатель. Там их целая серия, поэтому, если не смотрели, посмотрите. Получилось очень прикольно, на мой взгляд)
Так что создавайте потоки осознанно и так, как больше подходит именно в вашей ситуации. Потому что способов - тьма.
Stay versatile. Stay cool.
#multitasking
В прошлом я рассказал, что можно запускать поток не только свободной функцией, но и методом какого-то объекта. Во время публикации этого поста у меня появилось ощущение, что со стороны не совсем понятно, как компилятор различает аргументы функции, саму функцию и объект, из метода которого потенциально поток может быть запущен. Поэтому сегодняшний пост по это.
В С++ есть такое понятие - callable objects. Это просто сущности, которые можно вызвать. Это функции, лямбды, функторы, методы и так далее. Как можно единообразно вызывать все эти сущности? Сходу в голову ничего не приходит, но в стандартной библиотеке начиная с С++17 есть такая функция std::invoke. Вот ее сигнатура:
template< class F, class... Args>
std::invokeresultt<F, Args...> invoke(F&& f, Args&&... args) noexcept
И существуют правила, по которым эта функция парсит свои аргументы. Но не только она, а все сущности, которые как бы "вовлекают" в работу callable объект. Открывайте окна, потому что щас немного духоты будет.
Пусть INVOKE(f, t1, t2, ..., tN) - выражение, обозначающее вовлечение в работу функционального объекта f. Тогда:
Если f - указатель на метод класса T:
t1 - объект типа Т, или ссылка на объект типа Т,
или ссылка на наследника типа Т, то этот INVOKE превращается в (t1.*f)(t2, ..., tN).
t1 - ни что из перечисленного выше(то есть указатель на объект), то INVOKE превращается в ((*t1).*f)(t2, ..., tN).
Если N=1 и f - указатель на член класса:
t1 - объект типа Т, или ссылка на объект типа Т,
или ссылка на наследника типа Т, то этот INVOKE превращается в t1.*f.
t1 - ни что из перечисленного выше(то есть указатель на объект), то INVOKE превращается в (*t1).*f.
Во всех других случая INVOKE разворачивается в обычный вызов функции f(t1, t2, ..., tN).
То есть конструктор std::thread, который внутри себя дергает std::invoke, на основе вот этих простых правил и решает, что ему на самом деле передали и как себя дальше вести.
Пройдитесь глазами по схеме выбора еще раз, чтобы лучше уложить ее себе в голову, потому что она не прям простая. Тут кстати есть отсылочка к моему давнему посту про вызов метода объекта через указатель. Там их целая серия, поэтому, если не смотрели, посмотрите. Получилось очень прикольно, на мой взгляд)
Так что создавайте потоки осознанно и так, как больше подходит именно в вашей ситуации. Потому что способов - тьма.
Stay versatile. Stay cool.
#multitasking
Пример выстрела в лицо с помощью std::auto_ptr
Здесь мы рассмотрели проблемы std::auto_ptr. Проблемы понятные, но сегодня на практике разберем, как все может пойти не плану.
Прикол в том, что разработчики реализаций стандартной библиотеки понимали, люди так или иначе хотели пользоваться контейнерами, которые в себе содержали auto_ptr. Желание очевидное и поэтому разработчики делали такие реализации, которые сглаживают углы и косяки авто указателя. И в целом, хоть это и нестандарт, но с ними можно было работать.
Отчасти поэтому привести хороший пример того, как может выстрелить в ногу использование auto_ptr в контейнерах - задача не самая тривиальная. Но я вот попробую показать все эти интересности(см картинку).
Реализуем простенький класс с правилом нуля. Создадим вектор из авто указателей на объекты этого класса. Проинициализируем его.
И попробуем отсортировать. После вывода отсортированных элементов понимаем, что все сработало как надо и чиселки выстроились по порядку.
Однако теперь попробуем отсортировать с копирующей лямбдой. И вот тут-то мы и сегфолтнемся. При сортировке указатели будут копироваться в лямбду, то есть будут передавать туда владение ресурсом, а в контейнере будет оставаться фига. Поэтому при попытке снова получить доступ к полю ресурса мы наткнемся на нулевой указатель и упадем.
Да, сложно представить, что кто-то в проде напишет вторую лямбду(копирующую), но чем черт не шутит. Да и это лишь пример. Реальные ситуации могут быть сложные и неочевидные, поэтому будет легко попасться в ловушку. Это все-таки благосклонность компилятора, а не стандартная штука, на которую можно положиться в любых обстоятельствах.
Всем мув-семантики и непадающих программ.
Prevent your downfalls. Stay cool.
#compiler #STL #NONSTANDARD
Здесь мы рассмотрели проблемы std::auto_ptr. Проблемы понятные, но сегодня на практике разберем, как все может пойти не плану.
Прикол в том, что разработчики реализаций стандартной библиотеки понимали, люди так или иначе хотели пользоваться контейнерами, которые в себе содержали auto_ptr. Желание очевидное и поэтому разработчики делали такие реализации, которые сглаживают углы и косяки авто указателя. И в целом, хоть это и нестандарт, но с ними можно было работать.
Отчасти поэтому привести хороший пример того, как может выстрелить в ногу использование auto_ptr в контейнерах - задача не самая тривиальная. Но я вот попробую показать все эти интересности(см картинку).
Реализуем простенький класс с правилом нуля. Создадим вектор из авто указателей на объекты этого класса. Проинициализируем его.
И попробуем отсортировать. После вывода отсортированных элементов понимаем, что все сработало как надо и чиселки выстроились по порядку.
Однако теперь попробуем отсортировать с копирующей лямбдой. И вот тут-то мы и сегфолтнемся. При сортировке указатели будут копироваться в лямбду, то есть будут передавать туда владение ресурсом, а в контейнере будет оставаться фига. Поэтому при попытке снова получить доступ к полю ресурса мы наткнемся на нулевой указатель и упадем.
Да, сложно представить, что кто-то в проде напишет вторую лямбду(копирующую), но чем черт не шутит. Да и это лишь пример. Реальные ситуации могут быть сложные и неочевидные, поэтому будет легко попасться в ловушку. Это все-таки благосклонность компилятора, а не стандартная штука, на которую можно положиться в любых обстоятельствах.
Всем мув-семантики и непадающих программ.
Prevent your downfalls. Stay cool.
#compiler #STL #NONSTANDARD
Реальная ценность ссылок
Да и помните, как вводятся ссылки в учебных целях? Помню читал, что они нужны, чтобы внутри функции изменять переменную и эти изменения отразились в вызывающем блоке кода. И это удобнее указателя, потому что не нужно его разыменовывать. Все. С таким подходом естественно, что никто нихрена не понимает предназначения ссылок. Может только я так криво читал книжки. Это очень на меня похоже. Но раз я такой есть, значит есть и другие, похожие на меня. Поэтому этот пост для всех тех, кто читаешь книжки затылком, или просто новичков, которые не писали много кода.
Как бы функционал-то правильный, никто не спорит. С помощью ссылки в функции действительно можно проводить манипуляции с тем же самым объектом, что мы в нее передали и эти изменения отражаются на этом оригинальном объекте. На этом все основано. Но из этого поначалу довольно сложно сформулировать реальные кейсы использования ссылок. Собсна, погнали их разбирать.
У них есть 5 функции(ну или это я столько придумал)
1️⃣ Предотвращение лишнего копирования объекта при передаче в функцию. Ссылка - обертка над указателем, поэтому она занимает всего 4|8 байт и позволяет получить доступ к памяти, где находится объект. Опять же, наверняка в книжках объясняется, что ссылка - это обертка, но, как мне кажется, черезчур большой акцент делается на возможности изменения объекта, на который ссылается псевдоним. В очень многих ситуациях, когда в функции один из параметров - ссылка, она используется как read-only сущность. Тогда она помечена как const. Поэтому она лишь задает значение другим сущностям. А раз так, то нам в целом и нужен этот функционал изменения оригинального объекта и можно подумать, что в этом случае нужно по значению принимать аргумент. А вот нет. Тогда у нас будет дополнительное копирование. Нам такого не нужно. Мы общество без лишних копирований! Поэтому можно воспользоваться свойством, что ссылка - обертка над указателем. А значит мы можем с ее помощью без копирований задать значение другим объектам.
2️⃣ Output параметры. Уже ближе к способности изменения оригинального объекта. Иногда не хватает одного возвращаемого значения в функции, поэтому прибегают к использованию output параметров, чтобы функция передала нужную информацию через них. Есть конечно туплы или можно запилить отдельный класс, который в себе будет инкапсулировать нужные параметры, и возвращать его. Но туплы не очень информативные, так как у их элементов нет своих имен, а постоянно плодить сущности - не всегда удобно. Поэтому на помощь могут прийти output параметры. В этом случае они передаются по неконстантной ссылке. Просто в функцию передаются "пустые" объекты, то есть только что дефолтно созданные. И в этой функции на них нанизывается нужная информация. Которую мы потом может достать с помощью такой ссылочной семантики.
3️⃣ Изменение оригинального объекта. Эт вот та история, с которой я начал. Но! По моему опыту, это не самый популярный кейс использования ссылок. Попробую объяснить. Для кастомных классов очень часто приходится использовать std::shared_ptr, если время его жизни больше времени жизни скоупа. Тогда этот указатель везде передается по константной ссылке, хотя и объект, на который он указывает, можно изменять. Если нужно изменить какой-то объект, который создан на стеке, это нужно скорее делать через его собственные методы, а не сторонние функции. Так объект становится актором и проще воспринимать действия, которые происходят с ним происходят. Это вот та самая инкапсуляция.
4️⃣ Предоставление доступа к содержимому класса, без раскрытия приватных членов. Таким свойством обладает неконстантный operator[] для контейнеров STL. Вектору опасно предоставлять доступ к буфферу данных, где хранятся все объекты. Но более менее безопасно давать доступ к отдельным элементам для возможности их модификации. Это очень похожая на прошлый пример механика. Только в качестве текущего стейта выступает объект со своим содержимым, а модифицирующей функцией - текущая функция, в которой применяем operator[].
ПРОДОЛЖЕНИЕ В КОММЕНТАХ
#cppcore #goodpractice #design #STL
Да и помните, как вводятся ссылки в учебных целях? Помню читал, что они нужны, чтобы внутри функции изменять переменную и эти изменения отразились в вызывающем блоке кода. И это удобнее указателя, потому что не нужно его разыменовывать. Все. С таким подходом естественно, что никто нихрена не понимает предназначения ссылок. Может только я так криво читал книжки. Это очень на меня похоже. Но раз я такой есть, значит есть и другие, похожие на меня. Поэтому этот пост для всех тех, кто читаешь книжки затылком, или просто новичков, которые не писали много кода.
Как бы функционал-то правильный, никто не спорит. С помощью ссылки в функции действительно можно проводить манипуляции с тем же самым объектом, что мы в нее передали и эти изменения отражаются на этом оригинальном объекте. На этом все основано. Но из этого поначалу довольно сложно сформулировать реальные кейсы использования ссылок. Собсна, погнали их разбирать.
У них есть 5 функции(ну или это я столько придумал)
1️⃣ Предотвращение лишнего копирования объекта при передаче в функцию. Ссылка - обертка над указателем, поэтому она занимает всего 4|8 байт и позволяет получить доступ к памяти, где находится объект. Опять же, наверняка в книжках объясняется, что ссылка - это обертка, но, как мне кажется, черезчур большой акцент делается на возможности изменения объекта, на который ссылается псевдоним. В очень многих ситуациях, когда в функции один из параметров - ссылка, она используется как read-only сущность. Тогда она помечена как const. Поэтому она лишь задает значение другим сущностям. А раз так, то нам в целом и нужен этот функционал изменения оригинального объекта и можно подумать, что в этом случае нужно по значению принимать аргумент. А вот нет. Тогда у нас будет дополнительное копирование. Нам такого не нужно. Мы общество без лишних копирований! Поэтому можно воспользоваться свойством, что ссылка - обертка над указателем. А значит мы можем с ее помощью без копирований задать значение другим объектам.
2️⃣ Output параметры. Уже ближе к способности изменения оригинального объекта. Иногда не хватает одного возвращаемого значения в функции, поэтому прибегают к использованию output параметров, чтобы функция передала нужную информацию через них. Есть конечно туплы или можно запилить отдельный класс, который в себе будет инкапсулировать нужные параметры, и возвращать его. Но туплы не очень информативные, так как у их элементов нет своих имен, а постоянно плодить сущности - не всегда удобно. Поэтому на помощь могут прийти output параметры. В этом случае они передаются по неконстантной ссылке. Просто в функцию передаются "пустые" объекты, то есть только что дефолтно созданные. И в этой функции на них нанизывается нужная информация. Которую мы потом может достать с помощью такой ссылочной семантики.
3️⃣ Изменение оригинального объекта. Эт вот та история, с которой я начал. Но! По моему опыту, это не самый популярный кейс использования ссылок. Попробую объяснить. Для кастомных классов очень часто приходится использовать std::shared_ptr, если время его жизни больше времени жизни скоупа. Тогда этот указатель везде передается по константной ссылке, хотя и объект, на который он указывает, можно изменять. Если нужно изменить какой-то объект, который создан на стеке, это нужно скорее делать через его собственные методы, а не сторонние функции. Так объект становится актором и проще воспринимать действия, которые происходят с ним происходят. Это вот та самая инкапсуляция.
4️⃣ Предоставление доступа к содержимому класса, без раскрытия приватных членов. Таким свойством обладает неконстантный operator[] для контейнеров STL. Вектору опасно предоставлять доступ к буфферу данных, где хранятся все объекты. Но более менее безопасно давать доступ к отдельным элементам для возможности их модификации. Это очень похожая на прошлый пример механика. Только в качестве текущего стейта выступает объект со своим содержимым, а модифицирующей функцией - текущая функция, в которой применяем operator[].
ПРОДОЛЖЕНИЕ В КОММЕНТАХ
#cppcore #goodpractice #design #STL
Переворачиваем число
Сегодня у нас будет задачка. Формулируется просто - дано 32-битное целое число x. Верните число, десятичные цифры которого стоят в обратном порядке относительно числа x. Если будет переполнение, верните 0.
Решение, в целом, довольно очевидное. И скорее это задача для братьев наших меньших, кто еще не достиг дзена плюсового(ну хотя бы 23.87% от него). Однако здесь очень много нюансов, на которых вы можете споткнуться. Именно в этом и "сложность" задачи и ее польза. Поэтому нужно очень тщательно тестировать решение, чтобы оно было действительно корректным.
Но без одного уточнения решение будет совсем легким. Нельзя использовать переменные, которые занимают больше 4-х байтов в памяти.
Также у меня появились мысли по формату публикаций задач на канале. Под постом с непосредственно задачей будут идти обсуждения для тех, кто не очень понимает, в правильную ли сторону он мыслит, и хочет обсудит обсудить "большие штрихи" в решении.
Далее я буду отправлять следом за задачей еще один пост, в котором люди могут присылать свои решения на всеобщее ревью. Разделение нужно, чтобы собственно отделить сомневающихся и уже разобравшихся с деталями.
Ответ будет публиковаться вечером в комментах к еще одному посту. Это предотвратит явное появление ответа в ленте для тех, кто присоединится к каналу позже, и возможно тоже захочет порешать без спойлеров.
Погнали решать)
Challenge your problems. Stay cool.
#задачки
Сегодня у нас будет задачка. Формулируется просто - дано 32-битное целое число x. Верните число, десятичные цифры которого стоят в обратном порядке относительно числа x. Если будет переполнение, верните 0.
Решение, в целом, довольно очевидное. И скорее это задача для братьев наших меньших, кто еще не достиг дзена плюсового(ну хотя бы 23.87% от него). Однако здесь очень много нюансов, на которых вы можете споткнуться. Именно в этом и "сложность" задачи и ее польза. Поэтому нужно очень тщательно тестировать решение, чтобы оно было действительно корректным.
Но без одного уточнения решение будет совсем легким. Нельзя использовать переменные, которые занимают больше 4-х байтов в памяти.
Также у меня появились мысли по формату публикаций задач на канале. Под постом с непосредственно задачей будут идти обсуждения для тех, кто не очень понимает, в правильную ли сторону он мыслит, и хочет обсудит обсудить "большие штрихи" в решении.
Далее я буду отправлять следом за задачей еще один пост, в котором люди могут присылать свои решения на всеобщее ревью. Разделение нужно, чтобы собственно отделить сомневающихся и уже разобравшихся с деталями.
Ответ будет публиковаться вечером в комментах к еще одному посту. Это предотвратит явное появление ответа в ленте для тех, кто присоединится к каналу позже, и возможно тоже захочет порешать без спойлеров.
Погнали решать)
Challenge your problems. Stay cool.
#задачки
Защищенные методы 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
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
Исправляем косяк 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
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
Единица трансляции
В недавнем прошлом на канале было много линковочной тематики. Соответственно это требует частого упоминания термина "единица трансляции" 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
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
Объединения условий в 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
Вычисления по короткой схеме
Базовое и очень важное понятие для программирования в принципе и на плюсах в частности. Встретил просто английский термин 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
Подробности про 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
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
Сумма трех
Надеюсь, что вы и ваши близкие живы и здоровы. Чтобы начать новую неделю со свежей головой и отвлеченными мыслями, предлагаю порешать задачку.
Есть знаменитая в кругах решателей алгосов задача - сумма двух. Решается просто, понимается просто. Идеальная задачка для начинающих. Но сегодня я задам вам похожий по формулировке, но уже не такой простой вопрос.
Дан массив интов 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.
#задачки