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

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

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

Спасибо, @Ivaneo, за любезно предоставленный примерчик.

Посмотрите на этот код:

#include <iostream>

int read;

int main()
{
std::ios_base::sync_with_stdio(false);
std::cin >> read;
}


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

А получите ошибку сегментации: прилетит сигнал SIGSEGV.

Почему так? Мы же ничего незаконного не делали! Просто пытаемся читать в переменную, заблаговременно отвязав С++ потоки от сишных.

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

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

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

Посмотрим на примере gcc.

В исходниках есть такое определение библиотечного вызова чтения:

/* Read NBYTES into BUF from FD.  Return the number read or -1.  */
ssize_t
__libc_read (int fd, void *buf, size_t nbytes)
{
return SYSCALL_CANCEL (read, fd, buf, nbytes);
}

weak_alias (__libc_read, read)


Таким образом символ read, соответствующий функции чтения данных из файлового дискриптора, является слабым(weak).

А переменная read из нашего кода является сильным символом из сегмента неинициализированных данных(bss).

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

То есть при запуске программы и попытке вызвать функцию read, будет "вызываться" переменная. Отсюда и сегфолт.

Все системные функции - это С функции. У них нет никаких неймспейсов, чтобы предотвращать клаш имен. Поэтому всегда избегайте имен, которые могут конфликтовать со стандартными библиотечными функциями(например readopenclosewriteexit).

Avoid name clash. Stay cool.

#cppcore #goodoldc #OS
19🔥11👍7🤯4❤‍🔥1
​​Объявления функций vs объявление переменной или зачем нужен extern
#новичкам

В С/С++ существует интересная механика - разделение на объявление сущности и ее определение. У этого есть серьезные причины:

👉🏿 Ускорение компиляции: изменения в реализации не требуют перекомпиляции всех файлов, использующих объявление.

👉🏿 Сокрытие реализации: инкапсуляция, предоставление только интерфейса (в .hpp) и скрытие деталей в .cpp.

👉🏿 Разрешение циклических зависимостей и возможность ссылаться на типы до их полного определения.

👉🏿 Организация кода и разделение ответственности.

И если с функциями более менее понятно. Есть объявление в виде обозначения сигнатуры функции. И определение в виде полного предоставления тела функции:

void foo(int a); // declaration

void foo(int a) {
std::cout << a << std::endl; // definition
}


С переменными все немного сложнее. Но начнем с легкого:

int i = 0;


Это определение хоть глобальной, хоть локальной переменной i и ее инициализация нулем. Все все понимают.

Если думать в аналогии с функциями, то легко дойти до мысли что:

int i;


это объявление переменной.

Но это не так!

Это в любом случае определение!

Если вы встретите такую строчку вне функции, то это определение переменной и ее инициализация нулем. А внутри функции - определение без инициализации(значение будет мусорное). Определение переменной всегда связано с созданием объекта и выделением памяти под него.

Тогда как сказать компилятору, что я буду использовать переменную с каким-то именем и типом, но не хочу создавать объект а буду сслаться на определение в другом месте?

Ровно для этого и нужно ключевое слово extern.

extern int i; // declaration

int main() {
std::cout << i << std::endl;
}

int i = 42;

// OUTPUT
// 42


До мейна мы сказали, что будем ссылаться на переменную i, определение которой лежит в другом месте. И сразу после мейна предоставляем это определение. Компилятор все понял и в итоге мы получаем 42 на консоли, а не ноль.

В этом случае строчка extern int i; является объявлением глобальной переменной i.

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

Для локальных переменных функций объявление не предусмотрено(оно и не нужно). Если попытаетесь сделать так:

int main() {
extern int i;
std::cout << i << std::endl;
}


То вы опять объявите глобальную переменную i.

То есть чисто объявить вы можете только глобальную переменную (про полям класса речь не идет) и только с помощью extern.

Declare your intentions. Stay cool.

#cppcore
1🔥21👍118🤣3
​​Почему локальные переменные инициализируются мусором?
#новичкам

Изучать С++ больно по многим причинам. Одна из них - есть вещи, которые просто надо принять на веру. И чтобы понять, почему эти вещи работают так, а не иначе, нужно много времени за кодингом и качать сиплюсплюсные и computer science бицепсы.

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

int main() {
int i;
std::cout << i << std::endl;
}
// POSSIBLE OUTPUT:
// 64


Ну и в дополнение: Откуда берется этот мусор и почему нельзя просто нулями заполнить?

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

Стек вызовов - пространство в памяти, которое содержит информацию об исполнении функций. Представим аналогию со стопкой книг. Каждая книга - небольшой кусочек памяти с информацией об отдельной функции(фрейм функции). Как только вы вызываете функцию, на стопку кладется новая книга(фрейм). Когда исполнение доходит до конца функции - сверху снимается книга. Это происходит автоматически на уровне машинных инструций.

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

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

int main() {
int i;
...
}


Мы говорим, что хотим иметь переменную i и надо выделить под нее память, то есть просто передвинуть указатель на стек с позиции X на позицию X+4. Само значение мы при этом не задаем.

Но значение-то все равно будет. И будет оно браться их тех битиков-байтиков, которые находились в куске памяти от X до X-4.

А кто-то из вас заранее знает, какие байтики там будут храниться?

Нет

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

Так как для вас все эти процессы невидимы, вы и получаете мусор в неинициализированной переменной. Но это не совсем мусор: это просто данные ранее выполнившейся функции.

Чтобы более наглядно представить себе процесс работы со стеком вызовов, можете воспользоваться этим визуализатором. Там примеры фиксированные. Если же хотите на собственных примерах посмотреть - можете поиграться тут.

"Ну окей. Мусор берется из данных предыдущих вызовов функций. Но до main'а-то ничего не вызывается! Откуда там мусор?"

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

"Ладно, выкрутился. Но зачем вообще этот мусор оставлять, почему нельзя память занулять?"

Да можно, почему нельзя. Просто это не делается. Концепция С/С++ - не плати за то, чем не пользуешься.

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

Understand the root cause. Stay cool.

#cppcore #os
👍3512🔥10❤‍🔥3
​​Почему тогда глобальные переменные зануляются?
#новичкам

Внимательные читатели 2-х последних постов могут задаться вопросом: "Почему в неинициализированных локальных переменных лежит мусор, а в глобальных - нули? Откуда такие двойные стандарты?".

Можно конечно сказать, что такие двойные стандарты определены в стандарте С++ и на этом можно закончить.

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

int i;

int main() {
std::cout << i << std::endl;
}
// OUTPUT is always
// 0


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

"Это конечно хорошо, но на вопрос вы так и не ответили, а только стрелками кидаетесь."

Это был важный переход, который и приведет нас к ответу.

Язык С зарождался исходя из потребностей развития операционной системы Unix. Поэтому некоторые особенности ОС интегрировались в язык.

Конкретно неинициализированные глобальные переменные в Unix хранятся в сегменте глобальной памяти .bss . При запуске программы ОС выделяла память под программу. Для сегмента bss ядру нужно было выделить участок памяти запрошенного размера. Но что должно было лежать в этой памяти? По соображениям безопасности и изоляции процессов ядро не могло отдать программе память с остаточными данными от предыдущих процессов. Самый простой и эффективный способ гарантировать это — заполнить выделенную память нулями.

И программисты на С стали естественным образом пользоваться этой особенностью: неинициализированные глобальные переменные занулялись.

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

Вот так обнуление стало стандартом С, а затем и С++.

Understand the root cause. Stay cool.

#cppcore #os
8👍3011🔥9😱1
​​Как запретить объекту создаваться на стеке? Классика
#новичкам

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

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

Очевидно, что-то надо колдовать с конструкторами, потому что именно они создают объект.

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

Если мы хотим создавать объект только на куче можно написать так:

class MyClass {
private:
MyClass() { std::cout << "MyClass created\n"; }
public:
~MyClass() { std::cout << "MyClass destroyed\n"; }
static std::unique_ptr<MyClass> create() {
return std::unique_ptr<MyClass>(new MyClass());
}
void hello() const {
std::cout << "Hello, MyClass!\n";
}
};

auto obj = MyClass::create();
obj->hello();


Здесь фабричный метод создает std::unique_ptr, который и будет хранить указатель на класс. std::make_unique нельзя использовать, потому что внутри нее мы не сможем вызвать приватный конструктор.

Можно также удалить операции копирования и перемещения, потому что скорее всего в таком виде логика операций с MyClass не подразумевает копирования и перемещения.

Такой подход может быть полезен, если:

👉🏿 размер объекта очень большой и просто не влезет в стек

👉🏿 хотите реализовать всякие паттерны, типа object pool или оопшной фабрики.

👉🏿 нужно обрабатывать ошибки создания объекта без исключений - в случае ошибки просто вернем nullptr.

👉🏿 вы хотите какой-то особый контроль времени жизни.

Мы также можем разрешить создавать объект только в глобальной области. Такой паттерн называется синглтон:

class Singleton {
public:
static Singleton& instance() {
static Singleton inst;
return inst;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
void foo() {}
private:
Singleton() = default;
~Singleton() = default;
};


Конструктор и деструктор приватные, копирование и перемещение вообще запрещены. И есть статический метод, который возвращает ссылку на инстанс класса. За счет того, что используется статическая локальная переменная inst, инициализация объекта гарантировано потокобезопасна(только один поток в итоге создаст объект). В этом воплощении паттерн получил название синглтон Майерса.

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

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

Limit wrong uses. Stay cool.

#design #cppcore
24👍14🔥7
Как запретить объекту создаваться на стеке? Экзотика
#опытным

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

Начнем с конца. Жизни объекта, конечно. Давайте сделаем приватным не конструктор, а деструктор. Тогда внешний код не сможет разместить такой объект на стеке, ведь он не сможет его удалить потом.

Однако удалять объект хочется. Для этого сделаем публичный статический метод destroy и фабричный метод, возвращающий умный указатель с кастомным делитером:

class MyClass {
public:
MyClass() { std::cout << "MyClass constructor\n"; }

private:
~MyClass() { std::cout << "MyClass destructor\n"; }

public:
static void destroy(MyClass* ptr) {
delete ptr;
}

static std::unique_ptr<MyClass, decltype(&destroy)> create() {
return std::unique_ptr<MyClass, decltype(&destroy)>(new MyClass(), destroy);
}
};


При выходе из скоупа юник разрушится и его деструктор дернет destroy, который сам дернет delete.

Публичный конструктор здесь не имеет большого смысла, но и так и так работает.

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

Можно(почти).

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

class Key {
private:
Key() = default;
friend class Factory;
};

class Object {
public:
explicit Object(const Key&) {} // требует ключ
};

class Factory {
public:
static std::unique_ptr<Object> create() {
return std::make_unique<Object>(Key());
}
};


Технически, все методы класса Object публичные. Но инстанс Key может создать только фабрика. Поэтому и создание инстанса Object возможно только через нее.

Есть даже идиома, называется passkey, которая приписывает примерно так и создавать объекты.

Класс ключа может быть определен и внутри фабрики, как приватный класс. Фабрики может и не быть в принципе, Key может находиться внутри Object. Но суть одна.

И напоследок совсем гадкий утенок. Делаем в заголовке forward declaration типа и объявляем фабричную функцию. В cpp определяем класс и функцию.

// object.hpp
class Object;
std::unique_ptr<Object> createObject();

// object.cpp
#include "object.h"
class Object {
public:
Object() = default;
~Object() = default;
};

std::unique_ptr<Object> createObject() {
return std::make_unique<Object>();
}


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

Explore exotic things. Stay cool.

#design #cppcore
🔥178👍8🤯1
Как запретить объекту создаваться на куче?
#новичкам

Про стек поговорили, не будем и кучу обижать.

Начать можно со знакомого подхода: приватный конструктор и фабрика, возвращающая объект по значению:

class OnlyStack {
OnlyStack() = default;
public:
static OnlyStack Create() { return {}; }
};


Если объект может быть создан некорректно, то вернуть можно std::optional, с помощью которого ошибку можно будет отловить:

class OnlyStack {
OnlyStack() {
if (rand() % 2) {
std::cout << "ERROR" << std::endl;
}
}
public:
static std::optional<OnlyStack> Create() { return {}; }
};


"На приватных конструкторах мы уже собаку съели. Даешь способ по-интереснее!"

Хорошо. Давайте будем радикальными. Просто удалим перегрузку оператора new для этого класса. Тогда вообще никто не будет в состоянии объект на куче создать:

class OnlyStack {
public:
OnlyStack() = default;
~OnlyStack() = default;

static void* operator new(std::size_t) = delete;
static void* operator new = delete;
};

OnlyStack obj; // OK
OnlyStack* p = new OnlyStack(); // ERROR


Вот так просто и без дополнительных приседаний.

Но можно играть не радикально, а хитро. Не удалим, а переопределим operator new так, чтобы он размещал объекты на готовом существующем статическом буфере. Реально рабочий примерчик довольно большой будет, плюс чтобы не углубляться в пучины кастомных аллокаторов, покажем только идею:

class PooledObject {
static char pool[1024];
static size_t offset;
public:
void* operator new(size_t s) {
if (offset + s > 1024) throw std::bad_alloc();
void* ptr = pool + offset;
offset += s;
return ptr;
}
void operator delete(void*) noexcept {
// Complicated logic
// or just ignore freeing memory
}
};
PooledObject* obj = new PooledObject();


Типа линейный аллокатор, при запросе нового объекта просто сдвигаем offset буфера.

Это конечно все интересно. Но вспомните пост, где мы разбирали вопрос "Где аллоцируются элементы std::array?" и задумайтесь. А что если целевой объект будет полем другого класса, который мы создаем на куче?

Тогда его расположение будет определяться тем, где находится объемлющий объект. То есть, если мы создаём объект Container на куче, то и все его поля, включая OnlyStack, окажутся на куче. Получается, что наш запрет на new OnlyStack не спасает от ситуации, когда OnlyStack становится членом другого класса, который кто-то создаёт через new.

class OnlyStack {
OnlyStack() = default;
public:
static OnlyStack Create() { return {}; }
};

struct Container {
Container() : obj{OnlyStack::Create()} {}
OnlyStack obj;
};

Container* p = new Container(); // OK


Пишите в комментах, если знаете, как обойти эту проблему)

Don't be so radical. Stay cool.

#cppcore #memory #design
👍1910🔥9
make_unique и приватный конструктор
#новичкам

Если вы хотите как-то по-особенному контролировать время жизни объекта, то с функциями std::make_unique и std::make_shared у вас могут быть проблемы.

struct Type {
static std::unique_ptr<Type> Create() {
return std::make_unique<Type>();
}
private:
Type() = default;
};

int main()
{
auto obj = Type::Create();
}


Этот код не скомпилируется. Все потому что для класса std::make_unique - это внешний код и ей нужен публичный конструктор для работы.

Это можно обойти, просто использовав явный вызов new:

struct Type {
static std::unique_ptr<Type> Create() {
return std::unique_ptr<Type>(new Type);
}
private:
Type() = default;
};

int main()
{
auto obj = Type::Create();
}


Но это же явный вызов new! Из всех утюгов твердят, что сырые указатели - наши враги и с ними надо вести ожесточенную войну!

Есть один вариант, как этого можно избежать(если сырые указатели вызывают у вас диарею). Давайте сделаем публичный конструктор, но сделаем один из его параметров приватным типом класса. Сделаем у подтипа приватный конструктор и добавим Type в друзья. Тогда создавать объект по-прежнему можно будет только с помощью фабричного статического метода, но разблокируется возможность использовать std::make_unique:

class Type {
class PrivateKey { // private struct of Type
PrivateKey() = default;
friend Type;
};
public:
Type(PrivateKey) {}

static std::unique_ptr<Type> Create() {
return std::make_unique<Type>(PrivateKey{});
}
};

int main() {
auto obj = Type::Create(); // OK

auto obj2 = Type(PrivateKey{}); // ERROR: PrivateKey is private
}


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

Спасибо комментаторам из поста про запрет создания объектов на стеке за идею для публикации!

Know your enemies. Stay cool.

#cppcore #design #memory
👍229🔥9😁1
​​std::terminate
#опытным

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

Большинство из вас скорее всего знает, что вызов std::terminate приводит к завершению программы из-за какой-то ошибки.

Но как именно приводит? И почему это очень плохо? - На эти вопросы постараемся сегодня ответить.

Термин "ошибка" - слишком неоднозначный. На самом деле есть как минимум 2 категории ошибок: от которых можно восстановиться и от которых нельзя.

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

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

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

[[noreturn]] void terminate() noexcept;


Эта функция, которая не может бросать исключений и не возвращает никакого значения. Непонятно, как можно обработать ошибку при аварийном завершении программы. И так, как программа завершается при вызове terminate, никакой другой код не должен продолжится после этого вызова.

Внутренний механизм вызова std::terminate() достаточно общий для такого рода функций. std::terminate() не выполняет всю работу сама. Вместо этого она вызывает обработчик события "уничтожения программы". Обработчик настраивается через функцию std::set_terminate и по умолчанию там стоит вызов std::abort. А это уже функция которая немедленно посылает сигнал SIGABRT текущему процессу. Стандартная реакция на этот сигнал — аварийное завершение программы.

В чем проблемы такого завершения программы? Ну помимо того, что мы уже довольно сильно накосячили, что вызвался std::terminate.

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

Поэтому очевидно, что просто не надо допускать ситуаций, которые приводят к вызову std::teminate. Их конечно много, но они вполне четко определены и их довольно просто избегать. Об этих ситуациях мы и поговорим в следующем посте.

Recover from your mistakes. Stay cool.

#cppcore
👍2714🔥5🤣2
Сколько раз можно разыменовать лямбду?
#опытным

Давайте для начала поговорим о том, сколько раз можно разыменовать указатель?

Зависит от того, какой указатель мы имеем ввиду.

Тип int* можно разыменовать всего один раз. Получившийся объект после разыменования будет lvalue int, для которого отсутствует перегрузка operator*:

int i = 5;
int * p = &i;
std::cout << **p << std::endl; // error: indirection requires pointer operand ('int' invalid)


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

int i = 5;
int * p = &i;
int ** p1 = &p;
std::cout << **p1 << std::endl; // OK
std::cout << ***p1 << std::endl; // Error


Сколько же раз можно разыменовывать лямбду?

Так стоп. Как в лямбде можно применять оператор?

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

Так сколько?

Бесконечно

#include <iostream>

int main() {
auto ret = *****************************************[]{ return 23; };
std::cout << ret() << std::endl;
}


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

#ЧЗХ?

Давайте подумаем, что происходит при разыменовании лямбды. Она приводится к указателю на функцию, оператор применяется и результатом мы получаем lvalue функции(саму функцию, а не указатель). Тип этого выражения – int().

А функции у нас что любят делать? Правильно, неявно преобразовываться к указателю на саму себя.

Поэтому можем применить оператор еще разик.

А потом еще и еще. И еще, и еще, и еще, и еще... Ну вы поняли.

В любом случае ret по итогам того же неявного преобразования будет иметь тип указателя на фукнцию:

static_assert(std::is_same_v<decltype(ret), int(*)()>);


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

Спасибо, @Ivaneo, за любезно предоставленный примерчик.

Be limited only by your imagination. Stay cool.

#cppcore
29👍16😁11🔥2🤯1
Дефолтный конструктор. Введение
#новичкам

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

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

class NoConstructor {
int total;
public:
void accumulate (int x) { total += x; }
};

Example ex;


Казалось бы, у Example вообще не определено ни одного конструктора. Но тем не менее объект успешно создался. Как так?

Дефолтный конструктор умеет за вас генерировать сам компилятор.

А почему бы и нет? Смотря со стороны даже вполне логично и понятно, что он должен делать: вызывать конструкторы по умолчанию для всех нестатических членов и базовых классов в порядке объявления. Если мне от дефолтного конструктора нужно только это, то я могу просто положиться на компилятор.

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

Однако, не все так просто.

Как только вы определите хотя бы один другой конструктор, компилятор перестанет генерить дефолтный:

class ParametrizedConstructor {
int total;
public:
ParametrizedConstructor(int initial_value) : total(initial_value) { };
void accumulate (int x) { total += x; };
};
ParametrizedConstructor ex (100); // ok
ParametrizedConstructor ex1; // error: no default constructor


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

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

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

class ParametrizedAndDefaultConstructor {
int total;
public:
ParametrizedAndDefaultConstructor() = default; // или так
ParametrizedAndDefaultConstructor() {} // или так
ParametrizedAndDefaultConstructor(int initial_value) : total(initial_value) { };
void accumulate (int x) { total += x; };
};
ParametrizedAndDefaultConstructor ex(100); // ok
ParametrizedAndDefaultConstructor ex1; // ok


ParametrizedAndDefaultConstructor() = default; - вы явно определяете конструктор по-умолчанию, но так же явно просите компилятор о том, чтобы его поведение было как если бы сам компилятор его генерировал.

ParametrizedAndDefaultConstructor() {/something/} - это вы уже самостоятельно определяете, какую дополнительную логику должен иметь этот конструктор. Он делает все то же самое, что и тривиальный, только вдобавок выполняется еще и то, что вы указали внутри фигурных скобок. Такой конструктор называется уже нетривиальным, даже если его тело в итоге оказалось пустым.

О том, какую роль 50 оттенков конструктора по-умолчанию при обращении с объектами классов, мы поговорим в следующем посте.

Don't be trivial. Stay cool.

#cppcore
18👍12🔥6
Дефолтные конструкторы. Такие разные
#опытным

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

1️⃣ Тривиальный дефолтный конструктор

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

struct Foo {
int i;
double d;
char c;
};


На самом деле у всех тривиальных базовых типов в С++ есть конструктор по-умолчанию. Но он тривиальный. То есть ничего вообще не делает и никак не инициализирует объект.

И это свойство может передастся конструктору Foo. Если для такого типа дефолтный конструктор будет сгенерирован компилятором, то он тоже ничего делать не будет.

Конструктор по-умолчанию называется тривиальным, если:

👉🏿 У класса нет виртуальных методов.
👉🏿 Все базы класса имеют тривиальные конструкторы по-умолчанию.
👉🏿 Все поля класса имеют тривиальные конструкторы по-умолчанию.

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

2️⃣ Нетривиальный дефолтный конструктор, сгенерированный компилятором

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

Предоставив компилятору честь сгенерировать конструктор, вы разрешаете инициализировать объект с помощью агрегатной инициализации:

struct Foo {
// Foo() = default; так тоже можно
int i;
std::string s;
std::vector<int> v;
};

Foo f = {42, "Hello World", {1, 2, 3}};


можете даже designated initialization воспользоваться:

Foo f = {.i = 42, .s = "Hello World", .v = {1, 2, 3}};


Там есть конечно еще несколько требований, но опустим их, чтобы не сбивать фокус.

3️⃣ Нетривиальный пользовательский конструктор по-умолчанию

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

struct Foo {
Foo() {};
int i;
std::string s;
std::vector<int> v;
};

Foo f = {42, "Hello World", {1, 2, 3}}; // ERROR



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

Don't be trivial. Stay cool.

#cppcore
👍1713🔥6
​​Дефолтный конструктор. Ограничения
#новичкам

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

1️⃣ У класса есть другие конструкторы

class ParametrizedConstructor {
int total;
public:
ParametrizedConstructor(int initial_value) : total(initial_value) { };
void accumulate (int x) { total += x; };
};
Example2 ex (100); // ok
Example2 ex1; // error: no default constructor


Даже если вы сами определили copy/move конструктор руками, конструктор по-умолчанию неявно генерироваться не будет:

struct UserCopyConstructor
{
UserCopyConstructor(const UserCopyConstructor&) {}
// UserCopyConstructor::UserCopyConstructor() is implicitly defined as deleted
};

struct UserMoveConstructor
{
UserMoveConstructor(UserMoveConstructor&&) = default;
// UserMoveConstructor::UserMoveConstructor() is implicitly defined as deleted
};


2️⃣ В классе есть нестатическое поле-ссылка.

Ссылка обязательно должна быть инициализирована объектом, а это невозможно сделать, не имея объект.

struct HasReference {
int& ref; // error: reference needs to be initialized
};


3️⃣ В классе есть нестатическое константное поле с тривиальным конструктором по-умолчанию. Тривиальные конструкторы по сути вообще ничего не делают, кроме как начинают лайфтайм объекта. То есть все поля заполняются мусором.

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

struct ConstTrivial {
const int x; // int has a trivial default constructor, so cannot default construct an object
};


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

struct ConstNonTrivial {
const std::string s; // std::string has non-trivial constructor
};
ConstNonTrivial cnt; // OK, s is just empty string


4️⃣ Если какой-то член класса или базовый класс имеет недоступный (private) или удалённый (= delete) конструктор по умолчанию

struct NoDefault {
private:
NoDefault() {} // private constructor
};
struct Derived : NoDefault {
// Derived() is implicitly deleted
};


struct DeletedDefault {
DeletedDefault() = delete; // explicilty deleted
};
struct HasDeleted {
DeletedDefault dd;
};
HasDeleted hd; // ERROR! dd has explicilty deleted default constructor


Если знаете еще способы, пишите в комментах.

Don't be trivial. Stay cool.

#cppcore
13🔥7👍3😁2
​​Специальные методы классов
#новичкам

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

1️⃣ Конструктор по-умолчанию. Дефолтный конструктор без аргументов. Мы уже подробно разбирали его в предыдущих постах, останавливаться не будем.

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

2️⃣ Деструктор. Он семантически разрушает объект. Забирает у него память, закрывает дескрипторы и соединения, высасывает жизнь, отбирает квартиру, машину, жену и собаку и передает это государству операционной системе.

Как так получается, что компилятор вообще способен сам сгенерировать код деструктора объекта, который владеет ресурсами?

Все благодаря RAII и способности деструктора вызывать деструкторы своих полей. Если в вашем классе Foo содержится std::vector или std::unique_ptr, вам не нужно задумываться о менеджменте памяти, авторы этих классов уже все придумали за вас. Вам нужно лишь вызывать их деструктор, что деструктор Foo делает автоматически.

using File = std::unique_ptr<FILE, decltype(&std::fclose)>;

struct Foo {
std::vector<int> numbers;
File file;
// no explicit destructor ~Foo() {}
};

void foo() {
Foo obj = {{1, 2, 3, 4, 5, 6, 7, 8, 9}, {std::fopen("example.txt", "r"), &std::fclose}};
} // destroy obj here


3️⃣ Копирующий конструктор
4️⃣ Перемещающий конструктор

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

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

struct Foo {
std::vector<int> numbers;
// no explicit copy/move constructors
// Foo(const Foo& other) {...}
// Foo(Foo&& other) {...}
};

void foo() {
Foo obj = {{1, 2, 3, 4, 5, 6, 7, 8, 9}};
Foo copied_in_obj = obj; // copy obj
Foo moved_in_obj = std::move(obj); // move from obj


5️⃣ Копирующий оператор присваивания
6️⃣ Перемещающий оператор присваивания

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

И вот здесь интересная комбинация.

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

Чуть остановимся на синтаксисе:

Foo a, b;
a = b; // copy assign
a = std::move(b); // move assign

Foo a1 = b; // THIS IS NOT AN ASSIGN


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

Теперь примерчик:

struct Foo {
std::vector<int> numbers;
// no explicit copy/move constructors
// Foo& operator=(const Foo& other) {...}
// Foo& operator=(Foo&& other) {...}
};

void foo() {
Foo forward = {{1, 2, 3, 4, 5, 6, 7, 8, 9}};
Foo backward = {{9, 8, 7, 6, 5, 4, 3, 2, 1}};
Foo obj; // created obj here
obj = forward; // copy assign forward to obj
obj = std::move(backward); // move assign backward to obj


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

Be special. Stay cool.

#cppcore #cpp11 #interview
🔥10👍76