Грокаем C++
9.36K subscribers
44 photos
1 video
3 files
567 links
Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов.

По всем вопросам (+ реклама) @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Ковариантные возвращаемые типы

Есть такое интересное понятие, о котором вы возможно ни разу не слышали. Пример из поста выше с методами clone и create можно было написать иначе:

class Shape {
public:
virtual ~Shape() { } // A virtual destructor
// ...
virtual Shape* clone() const = 0; // Uses the copy constructor
virtual Shape* create() const = 0; // Uses the default constructor
};
class Circle : public Shape {
public:
Circle* clone() const override;
Circle* create() const override;
// ...
};
Circle* Circle::clone() const { return new Circle(this); }
Circle* Circle::create() const { return new Circle(); }


Вы скажете: "Сигнатуры не совпадают! Код не скомпилируется!".

А я скажу: "Shape и Circle - ковариантные типы". С++ разрешает наследнику переопределять методы с возвращаемым типом, который является наследником типа метода из базового класса. Говорят, что это даже называется идиомой С++.

Какие юзкейсы у этой идиомы? По факту всего один. Представьте, что все методы возвращают один тип Shape. Вы создали объект Circle в куче и присвоили указатель на него к указателю на Circle. Тогда при клонировании объекта Circle вам вернется указатель на объект базового класса. И по хорошему его надо динамик кастить к Circle, чтобы работать с конкретным типом наследника. А это оверхэд:

Circle *circle1 = new Circle();
Shape *shape = d1->clone();
Circle *circle2 = dynamic_cast<Circle *>(shape);
if(circle2) {
// Use circle2 here.
}


Выглядит не очень. Посмотрим, как изменится код, если методы Circle будут возвращать указатель на Circle:

Circle *circle1 = new Circle();
Circle *circle2 = d1->clone();


Выглядит намного лучше. Но вот вопрос: почему вы нигде не увидите в коде применения ковариантных типов?

Потому что этот подход не работает с умными указателями, которые де факто являются стандартом при возвращении объектов из фабрик. std::unique_ptr<Circle> не является наследником std::unique_ptr<Shape>, поэтому они и не ковариантные типы и сигнатуры методов будут несовместимы.

Возвращение сырых указателей - супер bad practice, один только этот факт заставляет отказаться от такого подхода.

Тем более полиморфные объекты и придумали для того, чтобы использовать их полиморфно. То есть через ссылку или указатель на базовый класс. Зачем оперировать полиморфным объектом с указателем на конкретный тип - не очень понятно.

Раньше, до изобретения умных указателей, идиома была легитимна. Теперь же она отправилась на свалку истории.

Только что вы прочитали очередную статью про совсем ненужную хрень. Ставьте 🗿, если ваше лицо сейчас на него похоже)

Stay poker-faced. Stay cool.

#fun #cppcore
🗿60👍27🔥96😁6🆒1
​​Приватный деструктор

Все мы с вами знаем, что можно делать конструкторы приватными. Например, для синглтон паттерна такое используется. Или для запрета создания объекта класса никаким другим образом, кроме как вызовом статический метода Create. Раньше, до появления возможности удаления функций в С++11 с помощью =delete, конструктор копирования делали приватным, чтобы запретить внешнему коду возможность копирования объекта.

Однако есть и симметричный сценарий, с которым вы явно не так часто сталкивались. Можно объявить приватным деструктор! Как это изменение отражается на поведении класса?

Вот у нас есть класс. Там не будет ничего, кроме приватного деструктора. И дальше мы попытаемся посоздавать объекты этого класса.

struct CreationTest {
private:
~CreationTest() {};
}

CreationTest global_obj;

int main() {
CreationTest auto_obj;
CreationTest * dynamic_obj = new CreationTest;
// delete dynamic_obj;
}


Пойдем по порядку. global_obj. Его конструктор вызывается в статической области памяти до вызова main. А деструктор по идее должен вызваться после завершения main в функции std::exit. Однако проблема: std::exit - внешний код для класса CreationTest, поэтому она не имеет право вызвать деструктор. Значит, на этой строчке будет ошибка компиляции. Вы не можете создавать объекты с приватным деструктором в статической области.

Далее auto_obj. Память под этот объект выделяется на стеке и конструктор вызывается на этой памяти. А деструктор этого объекта вызывается после выхода из скоупа самим рантаймом. У которого опять нет доступа к auto_obj. Да чтож такое. Опять ошибка компиляции.

Теперь dynamic_obj. Конструктор здесь вызывается самим оператором new, который в начале аллоцирует память и потом на этой памяти вызывает конструктор. С этим все хорошо. Но здесь намеренно допущена утечка, потому что если бы мы вызвали оператор delete, то и на этой строчке была бы ошибка.

То есть динамическая область - единственное место, где мы нормально можем создавать объекты. Но без удаления этих объектов жить будет как-то грустновато. Утечки памяти, ее фрагментация. В общем ничего хорошего. Нужно решать проблему гениально! Я подключаюсь к Галилео

Кто может получить доступ к приватным полям класса? Либо его методы, либо его кореша. То есть друзья. И это единственные сущности, которые помогут решить нам проблему. Покажу сразу оба варианта.

struct CreationTest {
static void Destroy(CreationTest * obj) {
delete obj;
}
friend void DestroyFunc(CreationTest * obj);
private:
~CreationTest() {};
}

void DestroyFunc(CreationTest * obj) {
delete obj;
}

int main() {
CreationTest * dynamic_obj = new CreationTest;
CreationTest::Destroy(dynamic_obj);
CreationTest * dynamic_obj1 = new CreationTest;
DestroyFunc(dynamic_obj1);
}


Теперь все компилируется без проблем.

Можно конечно немного позалупаться и создавать объекты через placement_new в автоматической области и также внутри функции вызывать деструктор, но это как будто бы борщ. Не очень удобно.

Кстати, можно для таких динамических объектов использовать и умные указатели с кастомными делитерами, чтобы не беспокоиться о ручном управлении памятью.

auto deleter = [](CreationTest * obj) {DestroyFunc(obj);};
std::unique_ptr<CreationTest, decltype(deleter)> smart_obj(new CreationTest, deleter);


В следующем посте поговорим о том, зачем вообще может понадобиться делать конструктор приватным.

Protect your private life. Stay cool.

#cppcore #cpp11
1🔥25👍1082
Зато летает...
🔥49🤣31👍5❤‍🔥42👏1
​​Зачем может понадобиться делать деструктор приватным?
#новичкам

Недавно мы узнали, что можно делать деструктор приватным. Те, кто не слышал о этой технике, скорее всего подумали: "Сомнительно........но.....окэй". Потому что с первого взгляда не очень понятно, для чего вообще нужно заниматься этими непристойностями in the first place. Чтобы запретить пользователю создавать объекты на стеке и в статической области?

Ну и действительно, этот паттерн используется нечасто. Однако кейсы применения все же есть.

Один из основных - ATL COM объекты. Их цикл жизни регулируется с помощью подсчета ссылок. И чтобы никто не смог удалить объект, на который все еще ссылаются ссылки, для объектов вводят приватный деструктор и метод Release. В нем происходит декремент ссылки, проверка их количества и удаление объекта, если он остался больше никому не нужным. Выглядеть это может примерно так:

int MyRefCountedObject::Release() 
{
_refCount--;
if ( 0 == _refCount )
{
delete this;
return 0;
}
return _refCount;
}


Как можно было понять, это довольно устаревшая и нишевая виндовая история. Раньше паттерн подсчета ссылок внутри самого класса был более популярен. Теперь, в эпоху стандартных умных указателей, это скорее странное легаси.

Помимо этого примера, для меня вообще довольно странная история, чтобы позволять какому-то коду создавать объект, но не позволять удалять его. Если я бы захотел контролировать удаление объекта, то с вероятностью 99% я бы захотел контролировать его создание. Чтобы так сказать стать черным властелином его цикла жизни. Поэтому, на мой взгляд, приватный деструктор должен идти вместе с приватным конструктором.

Для синглтонов имеет смысл сделать деструктор приватным, чтобы никто по ошибке не смог явно вызвать на ссылке деструктор. Сам деструктор вызывается в статическом методе-геттере, который имеет доступ к приватным полям, тут все аккуратно работает.

Также приватные конструктор и деструктор могут идти в комплекте с двумя статическими методами: Create() и Destroy().

Например, из Create возвращать умный указатель и указывать в качестве его делитера приватный статический метод Destroy. Таким образом вы можете реализовать довольно сложную логику создания и удаления объекта. Когда, например, у вас есть конструктор и метод init, то для ограничения работы с созданным, но не инициализированным объектом, используется статический метод Create. По аналогии для деструктора и finalize мы используем Destroy. Мы никого не ограничиваем в правильном создании и удалении объектов. Просто хотим, чтобы только через эти два метода проходило создание и удаление объектов.

class ControlLifeCycle
{
public:
static std::unique_ptr<ControlLifeCycle, void()(ControlLifeCycle)> Create() // Factory
{
auto deleter = [](ControlLifeCycle * obj)
{
ControlLifeCycle::Destroy(obj);
};
std::cout << "I was born!" << std::endl;
return std::unique_ptr<ControlLifeCycle, decltype(deleter)>(new ControlLifeCycle, deleter);
}
private:
static void Destroy(ControlLifeCycle* ptr)
{
std::cout << "I've been destroyed!" << std::endl;
delete ptr;
}
ControlLifeCycle() {} // Private CTOR and DTOR
~ControlLifeCycle() {}
};

ControlLifeCycle global_var; // error: ctor and dtor are private

int main ()
{
ControlLifeCycle stack_var; // error: ctor and dtor are private
ControlLifeCycle* dynamic_var = new ControlLifeCycle; // error: private ctor
auto smart_var = ControlLifeCycle::Create(); // OK
}


Можно из метода Create возвращать просто указатель и оставить метод Destroy публичным. Тогда только мы своими очумелыми ручками сможем напечатать в нужном месте кода "Class::Destroy(ptr);" и только тогда произойдет освобождение ресурсов и ни в каком другом случае.

В общем, эта техника может использоваться в любом месте, где нам необходим полный контроль над циклом жизни объекта и над способом его рождения и захоронения.

Control cycle of your life. Stay cool.
16👍10🔥62
​​nullptr
#новичкам

Вероятно, каждый, кто писал код на C++03, имел удовольствие использовать NULL и постоянно ударяться мизинцем ноги об этот острый уголок тумбочки. Дело в том, что NULL использовался, как обозначение нулевого указателя, который никуда не указывает. Но если он для этого использовался - это не значит, что он таковым и являлся. Это макрос, который мог быть определен как 0 aka int zero или 0L aka zero long int, но всегда это вариация интегрального нуля. И уже эти чиселки могли быть приведены к типу указателя.

Вот в этом-то и вся проблема. NULL очень явно хочет себя видеть в виде указателя, но по факту в зеркале видит число. Допустим, у нас есть 2 перегрузки одной функции: одна для инта, вторая для указателя:

class Spell { ... };

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

int main() {
castSpell(NULL);
}


Намерения ясны: мы хотим вызвать перегрузку для указателя. Но это гарантировано не произойдет! В произойдет один из двух сценариев: если NULL определен как 0, то просто без объявления войны в 4 часа утра вызовется вторая перегрузка. Если как 0L, то компилятор поругается на неоднозначный вызов: 0L может быть одинаково хорошо сконвертирован и в инт, и в указатель.

Проблему можно решить енамами и передавать для нулевого spellID что-то типа NoSpell. Но надо опять городить огород. Почему все не работает из коробки?!

С приходом С++11 начало работать из коробки. Надо только забыть про NULL и использовать nullptr.

Ключевое слово nullptr обозначает литерал указателя. Это prvalue типа std::nullptr_t. И nullptr неявно приводится к нулевому значению указателя для любого типа указателя. Это объект отдельного типа, который теперь к простому инту не приводится.

Поэтому сейчас этот код отработает как надо:

class Spell { ... };

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

int main() {
castSpell(nullptr);
}



Так как nullptr - значение конкретного типа std::nullptr_t, то мы может принимать в функции непосредственно этот тип, а не общий тип указателя. Такая штука используется, например, в реализации std::function, конструктор которого имеет перегрузку для std::nullptr_t и делает тоже самое, что и конструктор без аргументов.

/**
* @brief Default construct creates an empty function call wrapper.
* @post !(bool)this
/
function() noexcept
: _Function_base() { }

/
* @brief Creates an empty function call wrapper.
* @post @c !(bool)*this
/
function(nullptr_t) noexcept
: _Function_base() { }


По той же причине nullptr даже при возврате через функцию может быть приведен к типу указателя. А вот обычные null pointer константны не могут похвастаться таким свойством. Они могут приводиться к указателям только в виде литералов.

template<class T>
constexpr T clone(const T& t)
{
return t;
}
 
void g(int *)
{
std::cout << "Function g called\n";
}
 
int main()
{
g(nullptr); // Fine
g(NULL) // Fine
g(0); // Fine
 
g(clone(nullptr)); // Fine
// g(clone(NULL)); // ERROR: non-literal zero cannot be a null pointer constant
// g(clone(0)); // ERROR: non-literal zero cannot be a null pointer constant
}


clone(nullptr) вернет тот же nullptr и все будет работать гладко. А для 0 и NULL функция вернет просто int, который сам по себе неявно не конвертится в указатель.

Думаю, что вы все и так пользуете nullptr, но этот пост обязан быть на канале.

Как говорит одна древняя мудрость: "Use nullptr instead of NULL, 0 or any other null pointer constant, wherever you need a generic null pointer."

Be a separate subject. Stay cool.

#cppcore #cpp11
👍317🔥6🤔2
Найди летающих друзей

Сегодня будет не по С++ контент, но я не мог мимо этого пройти. Надеюсь вы сейчас никуда не спешите и у вас есть огнетушитель под рукой, будет жарко.

В общем, недавно в твиттере завирусился японский "'экспресс" тест на деменцию. Задача очень простая - найти на картинке бабочку, летучую мышь и утку. Все это надо сделать за 10 мин. Успели - молодцы. Не успели - скорее всего ваша разработческая карьера продлится не так долго, как вы этого ожидаете.

У меня не хватает усидчивости на такие штуки. Через 3 минуты безрезультатного поиска мне захотелось с криками "лайт вейт бэйбэээ" выкинуть что-нибудь тяжелое из окна и я понял, что пора залезать в комменты и ловить спойлеры. Буду верить, что раз я искал не 10 мин, это все не считается.

❤️ - нашел всех за 10 мин.
🤬 - где эта ср*ная бабочка?!

Keep calm. Stay cool.

#fun
297🤬30😁8🤔4🤯1😱1
​​Присвоение лямбды
#новичкам

Изучение лямбда выражений - не самая простая задача для начинающего плюсовика. Сложные термины, какие-то замыкания, списки захвата и прочее. В общем, непросто. И с виду можно подумать, что 2 одинаковые лямбды можно присваивать друг другу с легкостью. То есть может показаться, что такой код валидный:

int main() {
auto test = [](){};
test = [](){};

return 0;
}


Однако он генерирует примерно следующую ошибку:

In function ‘int main()’:
error: no match for ‘operator=’ in ‘test = <lambda closure object>main()::<lambda()>{}’
note: candidate is:
note: main()::<lambda()>& main()::<lambda()>::operator=(const main()::<lambda()>&) <deleted>
no known conversion for argument 1 from ‘main()::<lambda()>’ to ‘const main()::<lambda()>&’


Не нашел нужного оператора присваивания.

Да и вообще, это ж все лямбды, почему я не могу их присваивать друг другу?

Дело в том, что каждое отдельное лямбда-выражение генерирует свой тип, который называется замыканием. Только компилятор знает этот тип, вы его наружу никак не можете получить. Стандарт говорит:

The type of the lambda-expression [...] is a unique, unnamed non-union class type — called the closure type.


Это легко проверить. Такой код выведет 0:

auto test = [](){};
auto test2 = [](){};
std::cout << std::is_same_v<decltype( test ), decltype( test2 )> << std::endl;


Типы действительно разные.

В целом, поэтому вместе с лямбдами всегда нужно использовать auto. Потому что ничего другого вы вместо типа физически не сможете написать. Но компилятор знает тип замыкания и сможет вывести правильный тип для переменной.

Ну и естественно, для двух рандомных классов неопределены операторы присваивания своих объектов друг другу. Поэтому код из начала и фейлится.

Differentiate thing apart. Stay cool.

#cpp11
👍37❤‍🔥65🔥31😁1
​​Что на самом деле представляют собой short circuit операторы?

Мы уже узнали, что операторы && и || для кастомных типов - простые функции. Для функций существует гарантия вычисления всех аргументов перед тем как функция начнет выполняться. Поэтому перегруженные версии этих операторов и не проявляют своих короткосхемных свойств. Однако операторы && и || для тривиальных типов - другое дело и имеют такие свойства. Но почему? Как это так работает в одном случае и не работает в другом? Давайте разбираться.

Во-первых(и в-единственных), операторы для тривиальных типов - это не функции. Они сразу превращаются в определенную последовательность машинных команд. Так как у нас теперь нет ограничения, что мы должны вычислить все аргументы сразу, то и похимичить можно уже знатно.

Если подумать, то логика тут очень похожа на вложенные условия. Если первое выражение правдиво, переходим в вычислению второго, если нет, то выходим из условия(это для &&). И если еще подумать, то у нас и нет никаких других средств это сделать, кроме джампов(условных переходов к метке). Покажу, во что примерно компиляторы С/С++ преобразуют выражение содержащее оператор &&. Не настаиваю на достоверность и точность. Объяснение больше для понимание происходящих процессов.

Вот есть у нас такой код


if (expr1 && expr2 && expr3) {  
// cool operation
} else { 
// even cooler operation
}
// the coolest operation


Он преобразуется примерно вот в такое:


if (!expr1) goto do_even_cooler_operation; 
if (!expr2) goto do_even_cooler_operation; 
if (!expr3) goto do_even_cooler_operation; 

{
// cool operation
goto do_the_coolest_operation;


do_even_cooler_operation: 

// even cooler operation


do_the_coolest_operation:
// the coolest operation

Что здесь происходит. Входим в первое условие и если оно ложное(то есть expr1 - true), то проваливаемся дальше в следующее условие и делаем так, пока наши выражения правдивые. Если они в итоге все оказались правдивыми, то мы входим в блок выполняющий клевую операцию и дальше прыгаем уже наружу первоначального условия и выполняем самую клевую операцию. Если хоть одно из выражений expr оказалось ложным, то мы переходим по метке и выполняем еще круче операцию и естественным образом переходим к выполнению самой крутой операции. Прикол здесь в трех условиях. Так как они абсолютно не связаны друг другом и последовательны, то следующее по счету выражение просто не будет выполняться, пока выполнение не дойдет до него. Таким образом и обеспечиваются последовательные вычисления слева направо.

То есть встроенные операторы && и || разворачиваются вот с такую гармошку условий. Надеюсь, для кого-то открыл глаза, как это работает)

See what's under the hood. Stay cool.

#compiler #cppcore
👍20🔥115
Квиз

Мы с вами недавно коснулись темы лямбд, поэтому вдогонку устроим #quiz по этой теме. Как всегда, тут нужно либо хорошее знание стандарта, либо хорошая интуиция. Хотя интуиция поможет вам на квиз только правильно ответить, челюсть с пола она вам не поднимет, когда вы поймете, в чем дело.

Итак. Какой результат попытки компиляции(с одним флагом указания стандарта С++20) и выполнения этого кода?:

int main() {
auto test = +[]{};
test = []{};

return 0;
}


Ответ выйдет завтра.

Stay surprised. Stay cool.
🔥12👍53😁1
Магическое заклинание +[]{}

Правильный ответ на квиз - программа успешно завершится. Удивились? Погнали разбирать

Давайте сразу проясним, что лямбду можно записывать без списка параметров, это разрешенная тема. Да и работает это в целом довольно интуитивно: если ваша лямбда не принимает параметров, то вы можете их не указывать и все будет работать также, как и с пустыми круглыми скобками.

Дальше

Здесь мы рассматривали, что присвоение лямбд запрещено и приводит к ошибке компиляции. Каким образом один + переворачивает все сверх на голову и код работает?

Стандарт нам говорит:

The closure type for a lambda-expression with no lambda-capture has a public non-virtual non-explicit const conversion function to pointer to function having the same parameter and return types as the closure type's function call operator. The value returned by this conversion function shall be the address of a function that, when invoked, has the same effect as invoking the closure type's function call operator.


То есть ламбды без захваченых значений имеют возможность прикидываться указателями на функции.

И так вышло, что оператор+ определен для всех типов указателей в виде:

For every type T there exist candidate operator functions of the form

    T* operator+(T*);

Он ничего не делает с указателем и просто его возвращает наружу.

Когда мы ставим + спереди лямбды, компилятор пытается найти подходящую перегрузку для оператора+. Логично, что для любого замыкания он не перегружен, поэтому что он не нужен там. Но зато есть перегрузка для указателя, под который может косить лямбда. Что она успешно и делает.

Поэтому для здесь для test
auto test = +[]{};
auto test2 = []{};


компилятор выведет тип указателя на функцию, а не замыкания. И ничего нам не мешает перенаправлять этот указатель на другую функцию.

Итого, один бессмысленный плюс тащит катку.

Stay amazed. Stay cool.

#cpp11 #cppcore
31🔥14👍12🤯7😁2
​​Теория заговора

Вот живет программист по С++ своей прекрасной и беззаботной жизнью. Все у него хорошо: код пишется, баги фиксятся, деньги мутятся. И имя у него такое прекрасное - Иннокентий.

Иногда он лазается по cppreference, чтобы освежить знания по каким-то фичам или узнать что-то новое. Представим себе, что он зашел просмотреть на доку std::atoi и видит там такой фрагмент:
const auto data =
{
"42",
"0x2A", // treated as "0" and junk "x2A", not as hexadecimal
"3.14159",
"31337 with words",
"words and 2",
"-012345",
"10000000000" // note: out of int32_t range
};


Ничего необычного, просто определяется std::initializer_list<const char *> и записываются туда разные строки. Ну ладно, работает дальше.

А дальше ищет статейку по std::variant. И находит там вот какой отрывок:

int main()
{
std::variant<int, float> v, w;
v = 42; // v contains int
int i = std::get<int>(v);
assert(42 == i); // succeeds
w = std::get<int>(v);
w = std::get<0>(v); // same effect as the previous line
w = v; // same effect as the previous line
...
}


Почему-то он обратил внимание на число 42. "Где-то я его уже видел.". И вспоминает, что недавно видел это же число в коде для std::atoi. Это, конечно, немного странно - подумал, он. Но решил, что это просто случайность.

Приходит наш герой домой с работы и, как каждый уважающий себя программист, вместо ужина садится писать свой пет-проект. А вы не знали, что программисты могут годами жить на диете "код+кофе"?

Пишет он какое-то многопоточное приложение. Чтобы адекватно писать такие штуки, нужны глубокие знания о модели памяти в С++ и как работает синхронизация данных в многопроцессорном мире. Поэтому кодер снова идет на cppreference и находит там статейку про std::memory_order. Читает, читает. И херак, вылупил глаза в экран. "Это уже очень странно". А увидел он следующий фрагмент:

std::vector<int> data;
std::atomic<int> flag = {0};
 
void thread_1()
{
data.push_back(42);
flag.store(1, std::memory_order_release);
}


Опять это 42! "Что за приколы такие? Это что, любимое число плюсовиков, что они его везде пихают?". На том и порешил. Не нервничать же по поводу чьего-то любимого числа. Может именно на этот день рождения Страуструпу подарили маленького щеночка....

В общем, затерпели и забыли.

Иннокентий только начинает писать свой проект и ему нужен хороший сборочный скрипт. Выбор языка пал на модномолодежный питухон, поэтому он полез в документацию изучать базовый синтаксис питона. И тут на тебе:

x = int(input("Please enter an integer: "))
Please enter an integer: 42
if x < 0:
x = 0
print('Negative changed to zero')
elif x == 0:
print('Zero')
elif x == 1:
print('Single')
else:
print('More')


Whatafuck? Страуструп здесь уже никак не может быть замешан. Ситуация больше похожа на массонский заговор. Кенни не выдержал и пошел распутывать тайну века.

Оказалось, что это отсылка на книгу Дугласа Адамса "Автостопом по галактике". Там люди создали супермощный супекомпьютер только с одной целью - узнать ответ на "Главный вопрос жизни, Вселенной и всего такого". Этот вопрос настолько сложный и комплексный, что на нахождение ответа суперкомпьютер потратил целых 7.5 млн лет вычислений. И в окончании выдал: "42".

Роман вышел в период расцвета sci-fi, поэтому оставил глубокий отпечаток в массовой культуре. Оно появлялось в популярных сериалах типа "Остаться в живых". Даже один из радиотелескопов НАСА использует ровно 42 тарелки в честь отсылки к произведению.

Неудивительно, что гики по всему миру начали пихать это число во всех места в качестве пасхалки. Сейчас почти где-угодно встречая 42, вы можете быть на 99% уверены, что это именно отсылка на "Автостопом по галактике".

Так и была разгадана величайшая из тайн иллюминатов и Иннокентий довольный пошел спать. The end.

Make references to the great things. Stay cool.

#fun
👍5614🔥8😁7🗿4🤯3
Представление отрицательных чисел в С++

Отрицательные числа - заноза в заднице компьютеров. В нашем простом мире мы различаем положительные и отрицательные числа с помощью знака. Положительные числа состоят просто из набора цифр(ну и по желанию можно добавить + слева, никто не обидится), а к отрицательным слева приписывается минус. Но компьютеры у нас имеют бинарную логику, там все представляется в виде единиц и ноликов. И там нет никаких плюсов-минусов. Тогда если у нас модуль числа уже представляется в виде единичек и ноликов, то непонятно, как туда впихнуть минус.

Вот и в комитете по стандартизации не знали, как лучше это сделать и удовлетворить всем, поэтому до С++20 они скидывали с себя этот головняк. До этого момента С++ стандарт разрешал любое представление знаковых целых чисел. Главное, чтобы соблюдались минимальные гарантии. А именно: минимальный гарантированный диапазон N-битных знаковых целых чисел был [-2^(N-1) + 1; 2^(N-1)-1]. Например, для восьмибитных чисел рендж был бы от -127 до 127. Это соответствовало трем самым распространенным способам представления отрицательных чисел: обратному коду, дополнительному коду и метод "знак-величина".

Однако все адекватные компиляторы современности юзают дополнительный код. Поэтому, начиная с С++20, он стал единственным стандартным способом представления знаковых целых чисел с минимальным гарантированным диапазоном N-битных знаковых целых чисел [-2^(N-1); 2^(N-1)-1]. Так для наших любимых восьмибитных чисел рендж стал от -128 до 127.

Кстати для восьмибитных чисел обратной код и метод "знак-амплитуда" были запрещены уже начиная с С++11. Все из-за того, что в этом стандарте сделали так, чтобы все строковые литералы UTF-8 могли быть представлены с помощью типа char. Но есть один краевой случай, когда один из юнитов кода UTF-8 равен 0x80. Это число не может быть представлен знаковым чаром, для которого используются обратной код и метод "знак-величина". Поэтому комитет просто сказал "запретить".

Для кого-то много непонятных слов и терминов, поэтому в дальнейшем будем раскрывать все секреты представления отрицательных чисел в памяти.

Stay defined. Stay cool.

#cppcore #cpp20 #cpp11
20👍16🔥71❤‍🔥1
Квиз

Вчера в комментах наш подписчик Вячеслав скинул интересный примерчик, который здорово показывает вашу плюсовую интуицию. Поэтому захотелось разобрать его в рамках поста. Ну и для интереса предлагаю вам поучаствовать в #quiz и испытать свою интуицию. Код до боли краток, и до еще большей боли бессмысленен и ужасен. Но все же.

Какой будет результат попытки компиляции и запуска следующего кода?

#include <iostream>

int main () {
std::cout << +-!!"" << std::endl;
return 0;
}


Have a meaning in your life. Stay cool.

#fun
👍154🔥3🤬3
​​Ответ на квиз

В комментариях все более менее пришли к консенсусу, однако давайте разложим все по полочкам, чтобы вопросов уже точно не осталось.

#include <iostream>

int main () {
std::cout << +-!!"" << std::endl;
return 0;
}


Начнем с того, что этот код компилируется. Это уже много дает. По крайней мере вы смотрите на валидный С++ код.

Плюс-минусы и два восклицательных знака - валидные операции над валидными тривиальными типами
. Поэтому, по факту, единственное, что надо доказать - что !"" - валидное выражение.

Разберемся с кавычками. Это строковый литерал, обозначающий пустую строку. И он имеет тип const char[1]. Единичка берется из-за того, что сишные строки неявно содержат символ '\0' в конце. Так собственно и определяется конец строки.

Для массивов не определен оператор логического отрицания. Печаль...

Но зато он определен для указателей! А у массивов есть одно замечательное свойство - косить под указатель на свой первый элемент. Поэтому для выполнения !"", компилятор приведет "" к const char *, который будет указывать на какой-то конкретный участок памяти, где лежит эта пустая строка. Раз участок конкретный - тогда этот указатель считается типа true. Отрицаем true - получаем false.

Дальше остается всего-то +-!false. Отрицает false - получаем true. Также у нас есть встроеные унарные операторы для арифметических типов. А bool - тоже арифметический тип. Поэтому true - это как бы 1. +-1 в итоге дает -1(+знака числа не меняет).

Таким нехитрым образом, получаем ответ: -1.

Ставьте снеговика☃️, если ваша интуиция(или знания) вас не подвели.

Stay on positive side. Stay cool.
75🤯41🔥15👍138🆒2
А вы тоже делаете важный вид, что вы такой просветленный и знаете asm, а на деле просто надеетесь на то, что понимание работы этих закорючек само собой появится у вас в голове?
😁75🤣7❤‍🔥64🔥4🗿1
​​Знак-амплитуда

Начнем раскрывать тему вариантов представления отрицательных чисел с метода "знак-величина" или sign-magnitude. Это самый интуитивно понятный для нас способ репрезентации целых чисел. В обычной математике как: есть грубо говоря модуль числа и его знак.

И это довольно просто перенести в память. Мы просто обговариваем, что наиболее значимый бит числа мы отводим по бит знака, а остальные биты обозначают модуль числа. Если знаковый бит - 0, значит число положительное. Если 1 - отрицательное. Таким образом получается вполне естественная ассоциация с привычными нам правилами. Есть натуральный ряд чисел плюс ноль. Чтобы получить множество всех целых чисел, нужно ко множеству натуральных чисел прибавить множество чисел, которое получается из натуральных путем добавления минуса в начале числа.

Давайте на примере. В восьмибитном числе только 7 бит будут описывать модуль числа, который может находится в отрезке [0, 127]. А самый старший бит будет отведен для знака. С добавлением знака мы теперь может кодировать все числа от -127 до 127. Так, число -43 будет выглядеть как 10101011, в то время как 43 будет выглядеть как 00101011. Однако очень внимательные читатели уже догадались, что эта форма представления отрицательных чисел имеет некоторые side-effect'ы, которые усложняют внедрение этого способа в реальные архитектуры:

1️⃣ Появление двух способов изображения нуля: 00000000 и 10000000.

2️⃣ Представление числа из определения разбивается на 2 части, каждая из которых должна иметься в виду и каким-то образом обрабатываться.

3️⃣ Из предыдущего пункта выходит, что операции сложения и вычитания чисел с таким представлением требуют разной логики в зависимости от знакового бита. Мы должны в начале "отрезать" знаковый бит, сделать операцию, которая соответствует комбинации знаковых битов операндов и первоначальной операции, и потом обратно совместить результат со знаком.

4️⃣ Раз у нас 2 нуля, мы можем представлять на 1 число меньше, чем могли бы в идеале.

В общем, минусы значительные. И хоть такое представление и использовалось в ранних компьютерах из-за интуитивной понятности для инженеров, однако в наше время этот способ представления знаковых целых чисел практически не используется.

Однако "знак-величина" используется по сей день, но немного для другого. IEEE floating point использует этот метод для отображения мантиссы числа. Есть модуль мантиссы(ее абсолютное значение) и есть ее знаковый бит. Кстати поэтому у нас есть положительные и отрицательные нули, бесконечности и NaN'ы во флотах. Вот как оно оказывается работает.

Apply yourself in the proper place. Stay cool.

#cppcore #base
👍38❤‍🔥651😁1
Обратный код

Обратный код или ones' complement - уже более совершенное представление целых чисел в двоичном виде. Этот код позволяет очень легко выполнять операции сложения/вычитания над числами, используя только лишь операцию сложения.

Вообще говоря, в арифметике операцию вычитания можно переопределить через операцию сложения. Вместо вычитания одного числа из другого, вы инвертируете вычитаемое и складываете с уменьшаемым. Типа того:

a - b = a + (-b)


Обратный код очень удобно использовать именно в контексте такого свойства, потому что в нем очень просто инвертируются числа.

Как и во всех системах, поддерживающих представление отрицательных чисел, в нем есть зарезервированный знаковый бит, а затем идет "модуль" числа. Например, число 5 в двоичном виде представляется, как 101. Восьмибиное знаковое число 5 в обратном коде представляется, как 00000101. Старший бит 0 значит, что число положительное, дальше идут незначащие нули и в конце наше число.

Инвертированное же число получается просто обращением всех битов в обратные. 0 в 1 и 1 в 0.

То есть число -5 представляется, как 11111010.

И если мы сложим 2 обратных числа, то получим естественно 0:

00000101 + 11111010 = 11111111 = 0


Причем складываются два числа без учета "особенности" старшего бита. Сложение происходит просто как сложение двух двоичных чисел. Если сумма не умещается в заданное количество бит, то есть произошло "переполнение", то нужно сделать end-round carry aka добавление этого переполненного бита к сумме. Пока не очень понятно, но давайте попробуем что-нибудь посчитать:

31 - 12 = 31 + (-12) = 00011111 + (-00001100) = 00011111+ 11110011 = 1'00010010(произошло переполнение, поэтому отбрасываем старший бит и добавляем его к сумме) = 00010010 + 1 = 00010011 = 19


Получили ожидаемое положительное(знаковый бит 0) число 19.

Можем уйти в минуса:

25 - 29 = 25 + (-29) = 00011001 + (-00011101) = 00011001 + 11100010 = 11111011 = -00000100 = -4


Вот так все просто.

Но и у этого кода есть недостатки. Главный из них вы уже могли заметить в посте. Ноль представляется, как 11111111. Точнее, у нуля 2 значения - все нули и все единички. То есть появляется +0 и -0.

С точки зрения вычислений это особо никак не влияет на результат, но добавляет головной боли при разработке, так как надо тестировать софт и для отрицательного нуля.

Недостаток довольно надоедливый. Поэтому, и хоть динозавристые компьютеры использовали этот код, все современный машины используют дополнительный код. О нем в следующем посте.

Ну и вот этот end-round carry создает дополнительный этап вычисления: carry flag в процессоре нужно складывать с результатом.

Remove inconvenience from your life. Stay cool.

#base
1👍33🔥76🤔2🤨2
​​Дополнительный код

Вот мы и дошли до самого распространенного способа представления знаковых целых чисел в современных компьютерах. Это дополнительный код или two's complement на бездуховном.

По своей сути, это чуть усложненный обратный код. Если там для представления отрицательного числа нам нужно было инвертировать все биты числа и дело в шляпе, то здесь нам к получившемуся результату добавить единичку.

То есть:

-5 = ~5 + 1 = ~0000101 + 1 = 11111010 + 1 = 11111011


Дополнительный код также поддерживает замену операции вычитания через сложение с обратным вычитаемым. В случае переполнения просто откидываем новый получившийся разряд.

Можем проверить, кстати, что при сложении 2 обратных друг другу чисел, мы получим 0.

13 - 13 = 13 + (-13) = 13 + ~13 + 1 = 00001101 + 11110010 + 1 = 11111111 + 1 = 1'00000000 = 0


Именно за счет добавления единички мы избегаем ситуации, когда у нас есть 2 разных способа кодирования нуля.

Однако теперь у нас несимметричный диапазон представляемых чисел, так как место одного из нулей должно занять другое число. Если для обратного кода он был [-(2^(N-1) - 1), 2^(N-1) - 1] ([-127, 127] для восьмибитных чисел), то для дополнительного кода он такой [-2^(N-1), 2^(N-1) - 1] ([-128, 127] для восьмибитных чисел).

Вы наверное все это читаете и немного грустите. Во-первых, скучно. Во-вторых, это надо просто заучить(возможно только для экзамена). Никаких аналогий в голове на это особо не придумаешь.

Но на мой взгляд, здесь тот случай когда русскоязычное научное сообщество с переводами перемудрило, усложнив понимание. Сейчас все будет намного понятнее.

В английском достаточно знать слово complement и иметь немного воображения.

> Ones' complement (обратный код)
> Two's complement (дополнительный код).


Complement - дополнение. Грубо говоря если у вас есть часть предмета, то compliment это остальная часть, которой нужно дополнить вашу, чтобы получить целое.

В случае обратного кода (one's complement), отрицательное значение дополняет положительное так, чтобы их сумма давала в результате единицы во всех разрядах. Технически мы просто инвертируем биты, а идейно дополняем до всех единиц.

> (0) 000 -> 111 (-0)
> (1) 001 -> 110 (-1)
> (2) 010 -> 101 (-2)
> (3) 011 -> 100 (-3)

То есть: 000 + 111 = 001 + 110 = 010 + 101 = 011 + 100 == 111 - тот самый отрицательный ноль


Дополнительный код (two's complement) - похожий принцип. Здесь two (2) это основание системы счисления. Если у нас двоичная система счисления и есть N разрядов, то представление отрицательного значения должно дополнять представление положительного так, чтобы в сумме они давали 2^N. Например, для трёх бит 2^3 это (8) 1000. Следовательно:

> (0) 000 - у него нет "дополнения"
> (1) 001 -> 111 (-1)
> (2) 010 -> 110 (-2)
> (3) 011 -> 101 (-3)

001 + 111 = 010 + 110 = 011 + 101 = 1'000(2^3, откидываем старший разряд) == 0


А теперь еще немного магии для тех, кто путался, куда ставить апостроф в этих комплементах. Ones' complement -"дополнение до единиц". Единиц во множественном числе. То есть должны во всех разрядах получиться единицы при сложении с обратным. Two's complement - "дополнение до двойки". Двойки в единственном числе. Дополняем так, чтобы в сумме получилась степень двойки.

В английском апострофом обозначается принадлежность одного объекта другому. И для обозначения принадлежности существительным в единственном числе после слова идет апостроф и 's'. А для множественных просто апостроф(типа потому что s на конце уже есть).

Вот такие дела. Надеюсь, что последние абзацы мощно добавили вам понимания и раскрыли секреты вуду.

Have a deep meaning in your life. Stay cool.

#base
🔥25❤‍🔥6👍322