Как запретить объекту создаваться на стеке? Классика
#новичкам
Вопрос относится скорее к категории "из собеседований", но новичкам все равно полезно задавать себе такие вопросы, чтобы проверить глубину понимания С++.
Можно удалить все конструкторы с помощью пометки delete и тогда объект вообще нигде нельзя будет создавать. Но вопрос скорее всего про другое: как запретить объекту создаваться на стеке, но не на куче или статической памяти?
Очевидно, что-то надо колдовать с конструкторами, потому что именно они создают объект.
Прямолинейный подход - сделаем конструктор приватным. Тогда внешний код не сможет его вызвать.
Но создавать объект-то нам нужно все равно как-то. И для этого существуют фабричные методы - статические методы класса, которые дают доступ с объекту/его созданию.
Если мы хотим создавать объект только на куче можно написать так:
Здесь фабричный метод создает std::unique_ptr, который и будет хранить указатель на класс. std::make_unique нельзя использовать, потому что внутри нее мы не сможем вызвать приватный конструктор.
Можно также удалить операции копирования и перемещения, потому что скорее всего в таком виде логика операций с MyClass не подразумевает копирования и перемещения.
Такой подход может быть полезен, если:
👉🏿 размер объекта очень большой и просто не влезет в стек
👉🏿 хотите реализовать всякие паттерны, типа object pool или оопшной фабрики.
👉🏿 нужно обрабатывать ошибки создания объекта без исключений - в случае ошибки просто вернем nullptr.
👉🏿 вы хотите какой-то особый контроль времени жизни.
Мы также можем разрешить создавать объект только в глобальной области. Такой паттерн называется синглтон:
Конструктор и деструктор приватные, копирование и перемещение вообще запрещены. И есть статический метод, который возвращает ссылку на инстанс класса. За счет того, что используется статическая локальная переменная inst, инициализация объекта гарантировано потокобезопасна(только один поток в итоге создаст объект). В этом воплощении паттерн получил название синглтон Майерса.
Синглтоны могут быть полезны, например, при реализации пула соединений, логгера или сборщика метрик.
Обсудили по сути самые классические практически применимые способы. В следующем посте будет уже экзотика. Если знаете какие-то необычные способы - пишите в комментах, разберем их в следующий раз.
Limit wrong uses. Stay cool.
#design #cppcore
#новичкам
Вопрос относится скорее к категории "из собеседований", но новичкам все равно полезно задавать себе такие вопросы, чтобы проверить глубину понимания С++.
Можно удалить все конструкторы с помощью пометки 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 и фабричный метод, возвращающий умный указатель с кастомным делитером:
При выходе из скоупа юник разрушится и его деструктор дернет destroy, который сам дернет delete.
Публичный конструктор здесь не имеет большого смысла, но и так и так работает.
Но вообще говоря, зачем все эти запреты? Можно ли как-то добиться запрета при публичном конструкторе и деструкторе?
Можно(почти).
Давайте сделаем публичный деструктор и конструктор с параметром, который не может создать внешний код:
Технически, все методы класса Object публичные. Но инстанс Key может создать только фабрика. Поэтому и создание инстанса Object возможно только через нее.
Есть даже идиома, называется passkey, которая приписывает примерно так и создавать объекты.
Класс ключа может быть определен и внутри фабрики, как приватный класс. Фабрики может и не быть в принципе, Key может находиться внутри Object. Но суть одна.
И напоследок совсем гадкий утенок. Делаем в заголовке forward declaration типа и объявляем фабричную функцию. В cpp определяем класс и функцию.
В итоге да, мы запретили объекту создаваться, где угодно, кроме как через createObject. Но при этом пользоваться объектом мы никак не сможем. Только создать и удалить.
Explore exotic things. Stay cool.
#design #cppcore
#опытным
В этом посте будут не самые обычные способы запретов, которые вряд ли полезны на практике в таком виде, но полезны их отдельные элементы. Да и просто прикольные по своей идее.
Начнем с конца. Жизни объекта, конечно. Давайте сделаем приватным не конструктор, а деструктор. Тогда внешний код не сможет разместить такой объект на стеке, ведь он не сможет его удалить потом.
Однако удалять объект хочется. Для этого сделаем публичный статический метод 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
🔥17❤8👍8🤯1
Как запретить объекту создаваться на куче?
#новичкам
Про стек поговорили, не будем и кучу обижать.
Начать можно со знакомого подхода: приватный конструктор и фабрика, возвращающая объект по значению:
Если объект может быть создан некорректно, то вернуть можно std::optional, с помощью которого ошибку можно будет отловить:
"На приватных конструкторах мы уже собаку съели. Даешь способ по-интереснее!"
Хорошо. Давайте будем радикальными. Просто удалим перегрузку оператора new для этого класса. Тогда вообще никто не будет в состоянии объект на куче создать:
Вот так просто и без дополнительных приседаний.
Но можно играть не радикально, а хитро. Не удалим, а переопределим operator new так, чтобы он размещал объекты на готовом существующем статическом буфере. Реально рабочий примерчик довольно большой будет, плюс чтобы не углубляться в пучины кастомных аллокаторов, покажем только идею:
Типа линейный аллокатор, при запросе нового объекта просто сдвигаем offset буфера.
Это конечно все интересно. Но вспомните пост, где мы разбирали вопрос "Где аллоцируются элементы std::array?" и задумайтесь. А что если целевой объект будет полем другого класса, который мы создаем на куче?
Тогда его расположение будет определяться тем, где находится объемлющий объект. То есть, если мы создаём объект
Пишите в комментах, если знаете, как обойти эту проблему)
Don't be so radical. Stay cool.
#cppcore #memory #design
#новичкам
Про стек поговорили, не будем и кучу обижать.
Начать можно со знакомого подхода: приватный конструктор и фабрика, возвращающая объект по значению:
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
👍19❤10🔥9
make_unique и приватный конструктор
#новичкам
Если вы хотите как-то по-особенному контролировать время жизни объекта, то с функциями std::make_unique и std::make_shared у вас могут быть проблемы.
Этот код не скомпилируется. Все потому что для класса std::make_unique - это внешний код и ей нужен публичный конструктор для работы.
Это можно обойти, просто использовав явный вызов new:
Но это же явный вызов new! Из всех утюгов твердят, что сырые указатели - наши враги и с ними надо вести ожесточенную войну!
Есть один вариант, как этого можно избежать(если сырые указатели вызывают у вас диарею). Давайте сделаем публичный конструктор, но сделаем один из его параметров приватным типом класса. Сделаем у подтипа приватный конструктор и добавим Type в друзья. Тогда создавать объект по-прежнему можно будет только с помощью фабричного статического метода, но разблокируется возможность использовать std::make_unique:
По сути, это та же идиома passkey из этого поста. Только здесь она раскрывается с еще одной стороны.
Спасибо комментаторам из поста про запрет создания объектов на стеке за идею для публикации!
Know your enemies. Stay cool.
#cppcore #design #memory
#новичкам
Если вы хотите как-то по-особенному контролировать время жизни объекта, то с функциями 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
👍22❤9🔥9😁1
std::terminate
#опытным
Эта функция даже звучит страшно. Как будто бы даже есть немного опасений, что придет Шварц и заберет мою одежду, если в программе она вызовется.
Большинство из вас скорее всего знает, что вызов std::terminate приводит к завершению программы из-за какой-то ошибки.
Но как именно приводит? И почему это очень плохо? - На эти вопросы постараемся сегодня ответить.
Термин "ошибка" - слишком неоднозначный. На самом деле есть как минимум 2 категории ошибок: от которых можно восстановиться и от которых нельзя.
К первой категории можно отнести ситуации, от которых можно восстановиться. Когда системные вызовы возвращают отрицательное значение. Обычно это значит, что что-то пошло не так. Но жизнь программы продолжается: мы можем сделать ретрай или вообще прекратить исполнение задачи.
Туда же можно отнести выброс исключения. Ну ничего страшного: поймаем исключение, раскрутим стек, разрушим локальные объекты и продолжим исполнение дальше.
Ситуации же из второй категории намного серьезней. Вы в принципе нарушаете правила исполнения программы. Выход за границы массива - сегфолт. Слишком глубокая рекурсия - переполнение стека.
Таких ситуаций можно придумать много, и в каком-то небольшом их подмножестве вызывается функция std::terminate.
Эта функция, которая не может бросать исключений и не возвращает никакого значения. Непонятно, как можно обработать ошибку при аварийном завершении программы. И так, как программа завершается при вызове terminate, никакой другой код не должен продолжится после этого вызова.
Внутренний механизм вызова std::terminate() достаточно общий для такого рода функций. std::terminate() не выполняет всю работу сама. Вместо этого она вызывает обработчик события "уничтожения программы". Обработчик настраивается через функцию std::set_terminate и по умолчанию там стоит вызов std::abort. А это уже функция которая немедленно посылает сигнал
В чем проблемы такого завершения программы? Ну помимо того, что мы уже довольно сильно накосячили, что вызвался std::terminate.
Не раскручивается стек и не вызываются деструкторы ни локальных, ни глобальных объектов. Не вызываются никакие cleanup операции. Так или иначе при завершении программы ОС все равно заберет себе все ресурсы, но никакие процессы, происходящие в программе, грамотно не завершаются. Не флашатся потоки ввода-вывода и буферы, не возвращается нормальный ответ на запрос, не дообрабатываются уже готовые к обработке события. Батя ушел за хлебом и не вернулся... Ни свидетелей, ни весточки.
Поэтому очевидно, что просто не надо допускать ситуаций, которые приводят к вызову std::teminate. Их конечно много, но они вполне четко определены и их довольно просто избегать. Об этих ситуациях мы и поговорим в следующем посте.
Recover from your mistakes. Stay cool.
#cppcore
#опытным
Эта функция даже звучит страшно. Как будто бы даже есть немного опасений, что придет Шварц и заберет мою одежду, если в программе она вызовется.
Большинство из вас скорее всего знает, что вызов 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
👍27❤14🔥5🤣2
Сколько раз можно разыменовать лямбду?
#опытным
Давайте для начала поговорим о том, сколько раз можно разыменовать указатель?
Зависит от того, какой указатель мы имеем ввиду.
Тип
Но мы конечно же можем определить указатель на указатель и добиваться очень глубоких уровней индирекции. Однако мы все равно сможем разыменовать такой указатель ровно по количеству этих уровней, не больше:
Сколько же раз можно разыменовывать лямбду?
Так стоп. Как в лямбде можно применять оператор?
Лямбда без захвата неявно кастится к указателю на функцию. А указатель можно разыменовывать.
Так сколько?
Бесконечно
Чисто технически мы конечно ограничены количеством атомов во вселенной или, что более реально, возможностями компилятора обрабатывать длиннющие тексты программ. Но формальных ограничений нет.
#ЧЗХ?
Давайте подумаем, что происходит при разыменовании лямбды. Она приводится к указателю на функцию, оператор применяется и результатом мы получаем lvalue функции(саму функцию, а не указатель). Тип этого выражения –
А функции у нас что любят делать? Правильно, неявно преобразовываться к указателю на саму себя.
Поэтому можем применить оператор еще разик.
А потом еще и еще. И еще, и еще, и еще, и еще... Ну вы поняли.
В любом случае ret по итогам того же неявного преобразования будет иметь тип указателя на фукнцию:
По тем же рассуждениям, кстати, любой указатель на функцию тоже можно бесконечно разыменовать.
Спасибо, @Ivaneo, за любезно предоставленный примерчик.
Be limited only by your imagination. Stay cool.
#cppcore
#опытным
Давайте для начала поговорим о том, сколько раз можно разыменовать указатель?
Зависит от того, какой указатель мы имеем ввиду.
Тип
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
Дефолтный конструктор. Введение
#новичкам
Описание корректного интерфейса создания объекта - важная часть проектирования класса. Поэтому надо понимать нюансики работы с конструкторами, чтобы все правильно организовать.
Ну и конечно самый базовый и, потенциально, самый сложный с точки зрения языка - конструктор по-умолчанию.
Казалось бы, у Example вообще не определено ни одного конструктора. Но тем не менее объект успешно создался. Как так?
Дефолтный конструктор умеет за вас генерировать сам компилятор.
А почему бы и нет? Смотря со стороны даже вполне логично и понятно, что он должен делать: вызывать конструкторы по умолчанию для всех нестатических членов и базовых классов в порядке объявления. Если мне от дефолтного конструктора нужно только это, то я могу просто положиться на компилятор.
Таким образом конструктор по-умолчанию входит в число специальных методов классов, которые компилятор сам умеет генерить.
Однако, не все так просто.
Как только вы определите хотя бы один другой конструктор, компилятор перестанет генерить дефолтный:
Оно и понятно: если вы не определяли никакой конструктор, значит вы довольны дефолтным поведением. Но как только вы сами определили конструктор, вы сказали компилятору, что дефолтное поведение вам не подходит и вы берете ответственность за способы создания объектов этого класса.
И компилятор не смеет перечить вашей задумке. Очень может быть, что вы хотите, чтобы Example2 создавался только через параметрический конструктор и больше никак. По сути, именно это и прописано сейчас в классе. Если бы компилятор неявно добавил конструктор по-умолчанию, то это нарушило бы контракт вашего класса.
Если вы все-таки хотите, чтобы у вас была возможность создать объект по-умолчанию, то вам явно нужно добавить конструктор без аргументов:
О том, какую роль 50 оттенков конструктора по-умолчанию при обращении с объектами классов, мы поговорим в следующем посте.
Don't be trivial. Stay cool.
#cppcore
#новичкам
Описание корректного интерфейса создания объекта - важная часть проектирования класса. Поэтому надо понимать нюансики работы с конструкторами, чтобы все правильно организовать.
Ну и конечно самый базовый и, потенциально, самый сложный с точки зрения языка - конструктор по-умолчанию.
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; // okParametrizedAndDefaultConstructor() = default; - вы явно определяете конструктор по-умолчанию, но так же явно просите компилятор о том, чтобы его поведение было как если бы сам компилятор его генерировал.ParametrizedAndDefaultConstructor() {/something/} - это вы уже самостоятельно определяете, какую дополнительную логику должен иметь этот конструктор. Он делает все то же самое, что и тривиальный, только вдобавок выполняется еще и то, что вы указали внутри фигурных скобок. Такой конструктор называется уже нетривиальным, даже если его тело в итоге оказалось пустым.О том, какую роль 50 оттенков конструктора по-умолчанию при обращении с объектами классов, мы поговорим в следующем посте.
Don't be trivial. Stay cool.
#cppcore
❤18👍12🔥6
Дефолтные конструкторы. Такие разные
#опытным
Существует 3 вида дефолтных конструкторов и важно понимать, в чем у них различия, чтобы разрешать нужную функциональность.
1️⃣ Тривиальный дефолтный конструктор
По сути задача дефолтного конструктора - вызывать у своих полей и подклассов дефолтный конструктор. А что если все поля класса имеют тривиальные типы?
На самом деле у всех тривиальных базовых типов в С++ есть конструктор по-умолчанию. Но он тривиальный. То есть ничего вообще не делает и никак не инициализирует объект.
И это свойство может передастся конструктору Foo. Если для такого типа дефолтный конструктор будет сгенерирован компилятором, то он тоже ничего делать не будет.
Конструктор по-умолчанию называется тривиальным, если:
👉🏿 У класса нет виртуальных методов.
👉🏿 Все базы класса имеют тривиальные конструкторы по-умолчанию.
👉🏿 Все поля класса имеют тривиальные конструкторы по-умолчанию.
Все типы, которые совместимы с С, обладают этим свойством. Если вам нужна такая совместимость, тривиальный конструктор по-умолчанию - это ваш бро.
2️⃣ Нетривиальный дефолтный конструктор, сгенерированный компилятором
Если ваш класс содержит поле, у которого есть нетривиальный конструктор, то вам скорее всего и не нужна С-совместимость. Но вы можете приобрести кое-что другое.
Предоставив компилятору честь сгенерировать конструктор, вы разрешаете инициализировать объект с помощью агрегатной инициализации:
можете даже designated initialization воспользоваться:
Там есть конечно еще несколько требований, но опустим их, чтобы не сбивать фокус.
3️⃣ Нетривиальный пользовательский конструктор по-умолчанию
Как только вы сами определили дефолтный конструктор, то вы сразу же попали в эту категорию. И лишились преимуществ, описанных выше. Даже если вы определили пустой конструктор, он все равно считается кастомным:
Да, с инициализацией у С++ довольно сложные отношения, но много чего идет от наследия С и обратной совместимости.
Don't be trivial. Stay cool.
#cppcore
#опытным
Существует 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
👍17❤13🔥6
Дефолтный конструктор. Ограничения
#новичкам
Мы уже краем уха задевали эту тему, но сегодня поговорим основательно. Вот все причины, почему дефолтный конструктор не может неявно сгенерироваться компилятором:
1️⃣ У класса есть другие конструкторы
Даже если вы сами определили copy/move конструктор руками, конструктор по-умолчанию неявно генерироваться не будет:
2️⃣ В классе есть нестатическое поле-ссылка.
Ссылка обязательно должна быть инициализирована объектом, а это невозможно сделать, не имея объект.
3️⃣ В классе есть нестатическое константное поле с тривиальным конструктором по-умолчанию. Тривиальные конструкторы по сути вообще ничего не делают, кроме как начинают лайфтайм объекта. То есть все поля заполняются мусором.
Но для константных объектов подразумевается наличие определенного постоянного значения. Мусорные значения не удовлетворяют этому требованию.
Тем не менее, компилятор спокойно может проглотить константный член с нетривиальным дефолтным конструктором. Считается, что такой конструктор корректно инициализирует объект:
4️⃣ Если какой-то член класса или базовый класс имеет недоступный (private) или удалённый (
Если знаете еще способы, пишите в комментах.
Don't be trivial. Stay cool.
#cppcore
#новичкам
Мы уже краем уха задевали эту тему, но сегодня поговорим основательно. Вот все причины, почему дефолтный конструктор не может неявно сгенерироваться компилятором:
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 string4️⃣ Если какой-то член класса или базовый класс имеет недоступный (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 делает автоматически.
3️⃣ Копирующий конструктор
4️⃣ Перемещающий конструктор
Для этой пары конструкторов правило единое: самое простое решение для копирования/перемещения объекта - это просто скопировать/переместить все его поля в другой объект. Компилятор с этим вполне справится сам: вызовет пачку нужных конструкторов да и все.
Естественно, если хотя бы у одного поля вашего класса не будет копирующего/перемещающего конструктора, сам ваш класс потеряет возможность копироваться/перемещаться.
5️⃣ Копирующий оператор присваивания
6️⃣ Перемещающий оператор присваивания
Это операторы, которые вызываются тогда, когда вы хотите передать уже существующему объекту значение другого объекта.
И вот здесь интересная комбинация.
Чтобы присвоить значение, нужно уметь разрушать текущее значение и передавать другое значение. То есть автоматическая генерация присваивания возможно благодаря тому, что компилятор сам умеет разрушать объект и копировать/присваивать значения полей.
Чуть остановимся на синтаксисе:
Последняя операция не является присваиванием - это вызов конструктора(в этом случае копирующего). Если объект только создается, то это всегда вызов конструктора.
Теперь примерчик:
Итого: всего 6 специальных методов класса. Не больше, не меньше. На собесах любят про это спрашивать, так что забирайте.
Be special. Stay cool.
#cppcore #cpp11 #interview
#новичкам
Помимо обычных методов, которые позволяют классу выполнить какую-то полезную работу, существуют методы, которые менеджерят самими объектами. Среди них выделяют так называемые "специальные" методы классов, которые компилятор неявно за вас сгенерирует. Давайте перечислим их и поясним, почему компилятор способен вообще для них написать код:
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 obj5️⃣ Копирующий оператор присваивания
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👍7❤6