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

По всем вопросам - @ninjatelegramm

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

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

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

class Spell { };

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

int main() {
castSpell(NULL);
}


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

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

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

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

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

class Spell {};

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

int main() {
castSpell(nullptr);
}



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

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

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


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

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


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

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

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

Be a separate subject. Stay cool.

#cppcore #cpp11
Вывод типов
#новичкам

С++ - статически типизированный язык, что значит, что типы всех объектов должны быть известны на этапе компиляции. Это хорошо для безопасности программы и предсказуемости поведения, но не очень хорошо с точки зрения удобства написания программы. Не всегда мне хочется писать что-то типа "im::so::tired::of::typing::long<types>::iterator". Точнее никогда.

Да, есть алиасы и синонимы, это нужные и полезные вещи. Но не на все же гигадлинные типы их вводить.

Очень хочется, чтобы работу по "написанию" типов делал кто-то за нас. Ведь в конце концов, все сигнатуры функций и методов известны, нормальные пацаны используют явные плюсовые касты, инициализаторы обычно представляют из себя понятные типы. Да, есть всякие приколы с неявным приведением типов и неэксплисит конструкторами от одного аргумента. Но попробовать-то стоит?

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

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

std::unordered_map<std::string, std::vector<Customer>> data;


Так вот, чтобы по этой мапе проитерироваться раньше нужно было писать вот так:

for (std::unordered_map<std::string, std::vector<Customer> >::iterator it = data.begin(); it != data.end(); it++) {...}


Это конечно никуда не годится, выглядит ужасно, нечитаемо, да и код повторяется. Теперь подключаем 11-у плюсы и случается магия:

for (auto it = data.begin(); it != data.end(); it++) {...}


А добавив заклинание под называнием range-based-for, получим:
for (const auto& elem: data) {...}


Не идеально, это вам не питон. Но уже ощутимо приятнее и короче раза в 3.

Но тут встает вопрос: а как вообще эти типы-то выводятся? Есть наверное какие-то правила, алгоритм, по которому компилятор выводит тип?

Есть. Иначе это было бы магией(хотя грустновато без нее в нашем мире).

Его можно запомнить довольно легко. Поэтому в нескольких следующих постах мы будем разбирать эту тему.

А вообще знаете, что существует 3 вида вывода типов? Может и больше, но 3 точно есть, обещаю)

Delegate your work. Stay cool.

#cpp11
​​Опасности std::unordered_map
#опытным

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

В копилку полезности auto.

Вдруг вы решили не пользоваться этой фичей и пишите вот так:

struct Customer{
Customer(int num) : data{num} {}
Customer(const Customer& other) {
data = other.data;
std::cout << "Copy ctor" << std::endl;
}
private:
int data;
};
std::unordered_map<std::string, std::vector<Customer>> data;
data["qwe"] = {Customer{1}, Customer{2}};
for (const std::pair<std::string, std::vector<Customer>>& item : data) {
std::cout << "Idle print" << std::endl;
}


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

Copy ctor
Copy ctor
Idle print


При заполнении вектора кастомеры копируются из временных объектов, вызывается копирующий конструктор с принтом, и далее вывод цикла.

Однако на самом деле вывод будет такой:

Copy ctor
Copy ctor
Copy ctor
Copy ctor
Idle print


Мы этого совсем не ожидали. Откуда еще 2 копии?!!

Дело в том, что в нашей неупорядоченной мапе хранятся не std::pair<std::string, std::vector<Customer>>, а std::pair<const std::string, std::vector<Customer>>. Это в принципе особенность std::unordered_map: ключ мапы - неизменяемый объект, поэтому обобщенно мапа хранит std::pair<const Key, Value>.

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

Ну и естественно, эта проблема просто решается использованием ключевого слова auto.

struct Customer{
Customer(int num) : data{num} {}
Customer(const Customer& other) {
data = other.data;
std::cout << "Copy ctor" << std::endl;
}
private:
int data;
};
std::unordered_map<std::string, std::vector<Customer>> data;
data["qwe"] = {Customer{1}, Customer{2}};
for (const auto& item : data) {
std::cout << "Idle print" << std::endl;
}


Теперь у нас есть ожидаемый вывод.

Make your life easier. Stay cool.

#cpp11 #STL
​​ParamType - универсальная ссылка
#опытным

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

Только при такой сигнатуре шаблонной функции можно считать ее параметр универсальной ссылкой:

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

func(expression);


То есть это rvalue reference на cv-неквалифицированный тип. Только в таком виде тип param называется универсальной ссылкой. Как говорят в школе:
И ни в каком другом виде!

Ни
template <class T>
void func(std::vector<T>&& param) {...}

Это просто rvalue reference.
Ни
template <class T>
void func(const T&& param) {...}
Это тоже просто rvalue reference! Только константный.
И к последним двум кейсам применяются правила
отсюда
 и 
отсюда.


Когда expression - rvalue reference, то Т выводится безссылочным типом, чтобы тип ParamType был rvalue reference of T. Если тип expression - lvalue, то Т выводится в тип lvalue reference. Самое интересное, что это единственный кейс, когда тип Т выводится в ссылку.

Есть такое правило, что & + && = &. То есть при использовании универсальной ссылки в параметре шаблонной функции при передаче туда lvalue|lvalue reference, этот параметр выводится в lvalue reference. Это происходит именно за счет того, что шаблонный тип выводится в тип lvalue reference. Условно: функция принимает Т && , T выводится в int&, подставляем Т в параметр функции и получаем int& &&. Но такого синтаксиса нет и 2 ссылки коллапсируют в одну левую ссылку int&.


template<typename T> void f(T&& param); // param is a universal reference

int x = 27;
const int cx = x;
const int& lrx = x;
int&& rrx = 42;

f(x); // x is lvalue, so T is int&, param's type is also int&
f(cx); // cx is lvalue, so T is const int&, param's type is also const int&
f(lrx); // lrx is lvalue, so T is const int&, param's type is also const int&
f(27); // 27 is prvalue, so T is int, param's type is therefore int&&
f(std::move(rrx)); // rrx is xvalue, so T is int, param's type is therefore int&&


Обратите внимание на первые 3 кейса. Там Т выводится в lvalue reference тип. В двух последних Т - просто int безо всяких ссылок.

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

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

Stay universal. Stay cool.

#cppcore #cpp11 #template
​​const rvalue reference
#опытным

В прошлом посте мельком упомянул эту конструкцию, а в этом решил раскрыть по-подробнее.

Правые ссылки были введены в С++11 и с тех пор помогают в реализации семантики перемещения. С помощью таких ссылок мы можем убрать ненужное глубокое копирование объектов и внедрить "перемещение" одного объекта в другой. Достигается это с помощью специальных методов: конструктора перемещения и перемещающего оператора присваивания. Выглядит это так:

struct Movable {
Movable(int i) : num{new int(i)} {}
Movable(Movable&& other) {
num = other.num;
other.num = nullptr;
std::cout << "Don't have to copy in ctor\n";
}
Movable& operator=(Movable&& other) {
if (this != &other) {
delete num;
num = other.num;
other.num = nullptr;
std::cout << "Don't have to copy in assignment\n";
}
return *this;
}
~Movable() { delete num;}
int * num = nullptr;
}

Movable obj1{5};
Movable obj2{7};
Movable&& rvalue_ref = std::move(obj1);
Movable obj3{std::move(rvalue_ref)};
obj2 = std::move(obj3);

// OUTPUT
// Don't have to copy in ctor
// Don't have to copy in assignment


Эти два специальных метода всегда имеют такую сигнатуру. Даже если их генерирует за нас компилятор.

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

Так за каким хреном нам тогда нужны константный правые ссылки? Чтобы что? С первого взгляда это выглядит так: мы принимаем правые все правые ссылки в перегрузку(неконстантные ссылки биндятся к константным), но все равно копируем объект, потому что ничего другого сделать не можем. Звучит, как бред.

Но все же есть применение у этой конструкции.

Дело в том, что T&& могут кастится к const T&, T&& и const T&&. Наиболее подходящей перегрузкой будет T&&, дальше const T&& и, наконец, const T&. А вот левые ссылки к правым вообще не могут преобразовываться.

Соотвественно, если мы хотим принимать только lvalue в функцию и никак не пропускать правые ссылки, то "Хьюстон, у нас проблема!". Если мы просто определим перегрузку для const T&, то rvalue reference все равно смогут попадать в эту перегрузку. Что нас сильно огорчает.

Однако мы можем совершить ход конем: пометить перегрузку const T&& удаленной. Так как удаленные функции все еще участвуют в разрешении перегрузок, то для T&& более подходящим выбором будет const T&&, нежели const T&. Но мы намеренно удаляем эту перегрузку и тогда компилятор выдает ошибку из-за того, что не может найти подходящий вариант функции.

struct T{};

void f(T&) { std::cout << "lvalue ref\n"; }
void f(const T&) { std::cout << "const lvalue ref\n"; }
void f(const T&&) = delete; //{ std::cout << "const rvalue ref\n"; }

const T g() {
return T{};
}

int main() {
f(g()); // error: use of deleted function 'void f(const T&&)'
f(T{}); // error: use of deleted function 'void f(const T&&)'
}


Это применение и еще парочку других мы рассмотрим на реальных примерах в следующий раз.

Remove obstructing things from your life. Stay cool.

#cppcore #cpp11
​​Примеры использования const T&&
#опытным

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

Для чего это может быть нужно?

Допустим, мы храним в поле класса в каком-то виде ссылку на объект. И нам бы очень не хотелось принимать в конструкторе rvalue reference, потому что возможно сразу же после выхода из конструктора для объектов вызовется деструктор и хана этим объектам. И встречаем UB из-за хранения битой ссылки.

Есть такой стандартный класс std::reference_wrapper и его функции помощники std::ref() и std::cref(). Поскольку std::reference_wrapper предполагает хранение ссылки только для lvalue, то стандарт удалил перегрузки std::ref() и std::cref(), которые принимают const T&&.

template <class T> void ref(const T&&) = delete;
template <class T> void cref(const T&&) = delete;


По той же самой причине такая перегрузка удалена у функции std::as_const, которая формирует левую ссылку на константный тип из аргумента.

template< class T >  
constexpr std::add_const_t<T>& as_const( T& t ) noexcept;


Также константные правые ссылки используются в более сложных штуках, типа std::optional, когда нужно вернуть из него значение.

constexpr const T&& operator*() const&&;
constexpr const T&& value() const &&;


С этой же целью оно используется, например, и в std::get.

template< std::size_t I, class... Types >  
constexpr const std::variant_alternative_t<I, std::variant<Types...>>&&
    get( const std::variant<Types...>&& v );

template< class T, class... Types >
constexpr const T&& get( const std::variant<Types...>&& v );


В таких случаях использование const T&& оправдано передачей информации и о ссылочности типа, и о его константности. Это важно в обобщенном программировании, потому что никто не знает с каким типом будет работать шаблонная сущность. Вы вполне можете получить константный временный объект std::optional(да и любого другого объекта), это синтаксически корректно. И чтобы геттер его внутреннего значение отражал свойства обертки, приходится перегружать эти геттеры для любых возможных параметров. Так вот например методы std::optional упомянутые выше вызовутся только для временных константных объектов. И эти свойства отображаются в возвращаемом значении.

Также не стоит забывать, что константность объекта не накладывает ультимативных ограничений на использование объекта. Есть мутабельные и статические поля, которые можно изменять, и плевать они хотели на вашу константность. А также указатели. Мы не можем менять сам указатель, но можем изменить объект, на который он указывает. Это немного расширяет спектр возможностей использования константных правых ссылок, но не прям существенно. В голову пришел очевидный пример - pimpl idiom. Согласно этой идиоме класс хранит указатель на реализацию, в которой может лежать все, что угодно. Все операции, которые как-то изменяют состояние объекта, влияют на данные внутри указателя. Поэтому снаружи кажется, что объект и не изменился. Да и старый объект можно будет использовать. Непонятно только, зачем менять привычные традиции использования правых ссылок, но все же.

// MyClass.hpp
class MyClass {
public:
MyClass();
MyClass(int g_meat);
MyClass(const MyClass &&other); // const rvalue reference!
~MyClass();
int GetMeat() const;
private:
class Pimpl;
Pimpl *impl {};
};

// MyClass.cpp
class MyClass::Pimpl {
public:
int meat {42};
};

MyClass::MyClass() : impl {new Pimpl} { }

MyClass::MyClass(int g_meat) : MyClass() {
impl->meat = g_meat;
}

MyClass::MyClass(const MyClass &&other) : MyClass()
{
impl->meat = other.impl->meat;
other.impl->meat = 0;
}

MyClass::~MyClass() { delete impl; }

int MyClass::GetMeat() const {
return impl->meat;
}

// main.cpp
int main() {
const MyClass a {100500};
MyClass b (std::move(a)); // moving from const!
std::cout << a.GetMeat() << "\n"; // returns 0, b/c a is moved-from
std::cout << b.GetMeat() << "\n"; // returns 100500
}


Stay useful even if nobody understands you. Stay cool.

#template #cpp11 #STL
​​Зачем вообще нужен кастомный swap?

Коротко - незачем)

Но как всегда есть нюансы. Забайтились? Погнали разбираться.

Как всю историю человечества разделяет Рождество Христово, так и история С++ делится на две эпохи появлением стандарта С++11. Получается, что С++11 - Иисус в мире плюсов...

И вот до С++11 мы не имели семантики перемещения и функция std::swap обменивала два значения через копирование. Ну и естественно это никому не нравилось. Зачем такой оверхед, когда мне нужно только местами данные поменять?

И вот в те времена кастомная функция swap была как нельзя кстати. Именно поэтому std::vector имеет отдельный метод swap. Рудимент архаичного прошлого...

С тех пор все стандартные алгоритмы в первую очередь ищут use-defined swap и уже на крайняк используют std::swap.

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

Но по сути-то, свап - это такой одновременный мув друг в друга(идейно). Ну и с появлением мув-семантики стандратная swap стала выглядеть именно так, как нам нужно идейно:

template <typename T>
void swap(T& x, T& y)
{
T temp(std::move(x));
x = std::move(y);
y = std::move(temp);
}


Эта версия свапа делает ровно то, что ожидали практически от всех кастомных swap'ов - эффективный обмен двух значений.

Она позволяет даже некопируемым объектам, типа стримов, мьютексов и прочих, обменяться местами. То есть она буквально отобрала весь хлеб у кастомной swap: теперь стандратная функция делает такой же эффективный обмен значениями, плюс может также обменять некопируемые объекты. Красота!

Но у самописной swap остается одно преимущество: Не происходит никаких вызовов конструкторов классов обмениваемых объектов. Мы напрямую обмениваем содержимое объектов. А std::swap все-таки вызывает один мув-конструктор и 2 мув присваивания. О производительности надо думать...

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

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

Итог какой: кастомный своп был придуман, в основном, чтобы эффективно обменивать объекты. std::swap на стероидах мув-семантики позволяет делать это очень эффективно. Самописный своп имеет на первый взгляд незначительные преимущества по производительности. Но на практике как всегда надо тестировать оба варианта. Ну или не заниматься преждевременной оптимизацией и использоват std::swap.

Use standard things. Stay cool.

#cppcore #cpp11
​​Приватный деструктор

Все мы с вами знаем, что можно делать конструкторы приватными. Например, для синглтон паттерна такое используется. Или для запрета создания объекта класса никаким другим образом, кроме как вызовом статический метода 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
​​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
Представление отрицательных чисел в С++

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

Вот и в комитете по стандартизации не знали, как лучше это сделать и удовлетворить всем, поэтому до С++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