Неинициализированные переменные
Все мы знаем, что это плохо, ужасно, это проделки Сатаны и заговор евреев. Но нет-нет да и используем их. Сегодня кратко приведу еще один довод в пользу того, чтобы выкинуть из своей картины мира неинициализированные переменные.
Кейс из ревью.
Вот у вас есть примерно такая функция и примерно так ее можно вызвать:
Вроде нормальная функция, в случае ошибки не возвращает объекта, в случае успешного условия заполняет как-то объект, в случае неуспеха оставляет его дефолтным. И заполняется output параметр, сигнализирующий об наступлении условия.
Здесь success заполняется всегда вне зависимости, наступит условие или нет. И вроде как очень безопасно туда передать неинициализированный bool. Какая же разница? success в любом случае инициализируется в функции.
А вот приходит другой программист и изменяет эту функцию примерно так:
По логике реального кода было все четко. Кроме того, что теперь в ветке, где не выполняются одновременно condition1, condition2 и condition3 success остается тем же, что и передали в функцию.
Так конечно не нужно делать. Это хоть чуть-чуть, но изменяет логику поведения функции. Но я хочу тут другой момент затронуть.
Теперь при передаче неинициализированной success результат функции может быть дефолтовым, а success == true, так как в него изначально был записан true.
В реальности привело к довольно долгому дебагу.
Но если бы изначально мы создавали бы инициализированную в ложь переменную
то этой боли можно было бы избежать. А изменение функциональности самой функции выявят тесты.
Вывод: не используйте неинициализированные переменные! И включите уже соответствующую опцию компилятора, которая ругается на них ошибкой.
Define your value. Stay cool.
#cppcore #goodpractice
Все мы знаем, что это плохо, ужасно, это проделки Сатаны и заговор евреев. Но нет-нет да и используем их. Сегодня кратко приведу еще один довод в пользу того, чтобы выкинуть из своей картины мира неинициализированные переменные.
Кейс из ревью.
Вот у вас есть примерно такая функция и примерно так ее можно вызвать:
std::optional<SomeType> WaitForSomething(bool& success) {
SomeType result;
if (!SomeOperation()) {
// error
return std::nullopt;
}
if (condition) {
//fill result
}
success = (condition == 1);
return result;
}
bool success;
auto obj = WaitForSomething(success);
if (!obj) {
// handle error
}
if (success) {
// do something
}Вроде нормальная функция, в случае ошибки не возвращает объекта, в случае успешного условия заполняет как-то объект, в случае неуспеха оставляет его дефолтным. И заполняется output параметр, сигнализирующий об наступлении условия.
Здесь success заполняется всегда вне зависимости, наступит условие или нет. И вроде как очень безопасно туда передать неинициализированный bool. Какая же разница? success в любом случае инициализируется в функции.
А вот приходит другой программист и изменяет эту функцию примерно так:
std::optional<SomeType> WaitForSomething(bool& success) {
SomeType result;
if (!SomeOperation()) {
// error
return std::nullopt;
}
while (condition1 && condition2) {
//fill result
if (condition3)
success = true;
}
return result;
}По логике реального кода было все четко. Кроме того, что теперь в ветке, где не выполняются одновременно condition1, condition2 и condition3 success остается тем же, что и передали в функцию.
Так конечно не нужно делать. Это хоть чуть-чуть, но изменяет логику поведения функции. Но я хочу тут другой момент затронуть.
Теперь при передаче неинициализированной success результат функции может быть дефолтовым, а success == true, так как в него изначально был записан true.
В реальности привело к довольно долгому дебагу.
Но если бы изначально мы создавали бы инициализированную в ложь переменную
bool success = false;
то этой боли можно было бы избежать. А изменение функциональности самой функции выявят тесты.
Вывод: не используйте неинициализированные переменные! И включите уже соответствующую опцию компилятора, которая ругается на них ошибкой.
Define your value. Stay cool.
#cppcore #goodpractice
👍23🔥19❤4⚡2👎2
Все грани new
#опытным
Не каждый знает, что в плюсах new - это как медаль, только лучше. У медали 2 стороны, а у new целых 3!
Сейчас со всем разберемся.
То, что наиболее часто используется, называется new expression. Это выражение делает 2 вещи последовательно: пытается в начале выделить подходящий объем памяти, а потом пытается сконструировать либо один объект, либо массив объектов в уже аллоцированной памяти. Возвращает либо указатель на объект, либо указатель на начало массива.
Выглядит оно так:
Эта штука делает 2 дела одновременно. А что, если мне не нужно выполнять сразу 2 этапа? Что, если мне нужна только аллокация памяти?
За это отвечает operator new. Это оператор делает примерно то же самое, что и malloc. То есть выделяет кусок памяти заданного размера. Выражение new именно этот оператор и вызывает, когда ему нужно выделить память. Но его можно вызвать и как обычную функцию, а также перегружать для конкретного класса:
Но что, если у меня уже есть выделенная память и я хочу на ней создать объект? Допустим, я не хочу использовать кучу и у меня есть массивчик на стеке, который я хочу переиспользовать для хранения разных объектов, потенциально разных типов.
Тогда мне нужен инструмент, который позволяет только вызывать конструктор на готовой памяти.
Для этого есть placement new. Это тот же самый new expression, только для этого есть свой синтаксис. Сразу после new в скобках вы передаете указатель на область памяти, достаточной для создания объекта.
В этом случае кстати вызывается специальная перегрузка operator new:
Однако она не делает ничего полезного и просто возвращает второй аргумент наружу.
Вот так по разному в С++ можно использовать new. Главное - не запутаться!
Have a lot of sides. Stay cool.
#cppcore #memory
#опытным
Не каждый знает, что в плюсах new - это как медаль, только лучше. У медали 2 стороны, а у new целых 3!
Сейчас со всем разберемся.
То, что наиболее часто используется, называется new expression. Это выражение делает 2 вещи последовательно: пытается в начале выделить подходящий объем памяти, а потом пытается сконструировать либо один объект, либо массив объектов в уже аллоцированной памяти. Возвращает либо указатель на объект, либо указатель на начало массива.
Выглядит оно так:
// creates dynamic object of type int with value equal to 42
int* p_triv = new int(42);
// created an array of 42 dynamic objects of type int with values equal to zero
int* p_triv_arr = new int[42];
struct String {std::string str};
// creates dynamic object of custom type String using aggregate initialization
String* p_obj = new String{"qwerty"};
// created an array of 5 dynamic objects of custom type String with default initialization
String* p_obj = new String[5];
Эта штука делает 2 дела одновременно. А что, если мне не нужно выполнять сразу 2 этапа? Что, если мне нужна только аллокация памяти?
За это отвечает operator new. Это оператор делает примерно то же самое, что и malloc. То есть выделяет кусок памяти заданного размера. Выражение new именно этот оператор и вызывает, когда ему нужно выделить память. Но его можно вызвать и как обычную функцию, а также перегружать для конкретного класса:
// class-specific allocation functions
struct X
{
static void* operator new(std::size_t count)
{
std::cout << "custom new for size " << count << '\n';
// explicit call to operator new
return ::operator new(count);
}
static void* operator new[](std::size_t count)
{
std::cout << "custom new[] for size " << count << '\n';
return ::operator new;
}
};
int main()
{
X* p1 = new X;
delete p1;
X* p2 = new X[10];
delete[] p2;
}
Но что, если у меня уже есть выделенная память и я хочу на ней создать объект? Допустим, я не хочу использовать кучу и у меня есть массивчик на стеке, который я хочу переиспользовать для хранения разных объектов, потенциально разных типов.
Тогда мне нужен инструмент, который позволяет только вызывать конструктор на готовой памяти.
Для этого есть placement new. Это тот же самый new expression, только для этого есть свой синтаксис. Сразу после new в скобках вы передаете указатель на область памяти, достаточной для создания объекта.
alignas(T) unsigned char buf[sizeof(T)];
// after new in parentheses we specified location of future object
T* tptr = new(buf) T;
// You must manually call the object's destructor
tptr->~T();
В этом случае кстати вызывается специальная перегрузка operator new:
void* operator new (std::size_t count, void* ptr);
Однако она не делает ничего полезного и просто возвращает второй аргумент наружу.
Вот так по разному в С++ можно использовать new. Главное - не запутаться!
Have a lot of sides. Stay cool.
#cppcore #memory
30👍35❤18🔥8🏆3⚡1
new vs malloc
Чем отличаются new и malloc? Один из популярных вопросов на собеседованиях, которые проверяет, насколько хорошо вы знакомы с тонкостями работы с памятью в С/С++. Поэтому давайте сегодня это обсудим.
Не совсем корректно, наверное сравнивать фичи двух разных языков с разными доминантными парадигмами программирования. Но раз в стандарте есть std::malloc, а new тоже выделяет память, то можно попробовать.
👉🏿 new expression помимо аллокации памяти вызывает конструктор объекта. std::malloc только выделяет память.
👉🏿 std::malloc - совсем не типобезопасный. Он возвращает void * без какого-либо признака типа. Придется явно кастовать результат к нужному типу. new в свою очередь возвращает типизированный указатель.
👉🏿 При ошибке выделения памяти new бросает исключение std::bad_alloc, в то время как std::malloc возвращает NULL. Соответственно нужны разные способы обработки ошибочных ситуаций.
👉🏿 Поведение new может быть переопределено внутри кастомных классов, поведение std::malloc - неизменно.
👉🏿 Если вам не нужно конструирование объекта, то просто вызывайте operator new. Он делает то же самое, что и std::malloc(потенциально вызывает его внутри себя).
👉🏿 Для new не нужно вручную высчитывать количество нужных байт. То есть мы не лезем на низкий уровень. Мы заботимся только типе данных, количестве объектов и об аргументах конструктора.
👉🏿 new плохо работает с реаллокациями. Нужно выделить новый сторадж, скопировать туда данные и вызвать delete. В то время, как malloc имеет функцию-партнера realloc, которая может изменить размер существующего куска памяти более эффективно, чем последовательность new-memcpy-delete.
Однако они имеют одну неочевидную схожесть. Нужно стараться по максимуму избегать их явного вызова. Давно придумали умные указатели и контейнеры, которые позволяют максимально освободить разработчика от обязанности ручного управления памятью.
Мы все же современные плюсовики. Поэтому в большинстве случаев, вам не нужны будут прямые вызовы этих функций. В более редких случаях(например кастомные аллокаторы) можно явно использовать new. Ну и в совсем редких случаях(нужда в реаллокации памяти или работа с сишным кодом) можно использовать malloc.
Control your memory. Stay cool.
#cppcore #interview #memory
Чем отличаются new и malloc? Один из популярных вопросов на собеседованиях, которые проверяет, насколько хорошо вы знакомы с тонкостями работы с памятью в С/С++. Поэтому давайте сегодня это обсудим.
Не совсем корректно, наверное сравнивать фичи двух разных языков с разными доминантными парадигмами программирования. Но раз в стандарте есть std::malloc, а new тоже выделяет память, то можно попробовать.
👉🏿 new expression помимо аллокации памяти вызывает конструктор объекта. std::malloc только выделяет память.
👉🏿 std::malloc - совсем не типобезопасный. Он возвращает void * без какого-либо признака типа. Придется явно кастовать результат к нужному типу. new в свою очередь возвращает типизированный указатель.
👉🏿 При ошибке выделения памяти new бросает исключение std::bad_alloc, в то время как std::malloc возвращает NULL. Соответственно нужны разные способы обработки ошибочных ситуаций.
👉🏿 Поведение new может быть переопределено внутри кастомных классов, поведение std::malloc - неизменно.
👉🏿 Если вам не нужно конструирование объекта, то просто вызывайте operator new. Он делает то же самое, что и std::malloc(потенциально вызывает его внутри себя).
👉🏿 Для new не нужно вручную высчитывать количество нужных байт. То есть мы не лезем на низкий уровень. Мы заботимся только типе данных, количестве объектов и об аргументах конструктора.
👉🏿 new плохо работает с реаллокациями. Нужно выделить новый сторадж, скопировать туда данные и вызвать delete. В то время, как malloc имеет функцию-партнера realloc, которая может изменить размер существующего куска памяти более эффективно, чем последовательность new-memcpy-delete.
Однако они имеют одну неочевидную схожесть. Нужно стараться по максимуму избегать их явного вызова. Давно придумали умные указатели и контейнеры, которые позволяют максимально освободить разработчика от обязанности ручного управления памятью.
Мы все же современные плюсовики. Поэтому в большинстве случаев, вам не нужны будут прямые вызовы этих функций. В более редких случаях(например кастомные аллокаторы) можно явно использовать new. Ну и в совсем редких случаях(нужда в реаллокации памяти или работа с сишным кодом) можно использовать malloc.
Control your memory. Stay cool.
#cppcore #interview #memory
🔥41👍17❤9⚡2👎1
Безопасный для исключений new
#опытным
Большинство приложений не могут физически жить без исключений. Даже если вы проектируете все свои классы так, чтобы они не возбуждали исключений, то от одного вида exception'ов вы вряд ли уйдете. Дело в том, что оператор new может бросить std::bad_alloc - исключение, которое говорит о том, что система не может выделить нам столько ресурсов, сколько было запрошено.
Однако мы можем заставить new быть небросающим! Надо лишь в скобках передать ему политику std::nothow. Синтаксис очень похож на placement new. Это в принципе он и есть, просто у new есть перегрузка, которая принимает политику вместо указателя.
В первом случае при недостатке памяти выброситься исключение. А во втором случае - вернется нулевой указатель. Прям как в std::malloc.
Так что, если хотите избавиться от исключений - вот вам еще один инструмент.
#cppcore #memory
#опытным
Большинство приложений не могут физически жить без исключений. Даже если вы проектируете все свои классы так, чтобы они не возбуждали исключений, то от одного вида exception'ов вы вряд ли уйдете. Дело в том, что оператор new может бросить std::bad_alloc - исключение, которое говорит о том, что система не может выделить нам столько ресурсов, сколько было запрошено.
Однако мы можем заставить new быть небросающим! Надо лишь в скобках передать ему политику std::nothow. Синтаксис очень похож на placement new. Это в принципе он и есть, просто у new есть перегрузка, которая принимает политику вместо указателя.
MyClass * p1 = new MyClass; // привычное использование
MyClass * p2 = new (std::nothrow) MyClass; // небросающая версия
В первом случае при недостатке памяти выброситься исключение. А во втором случае - вернется нулевой указатель. Прям как в std::malloc.
Так что, если хотите избавиться от исключений - вот вам еще один инструмент.
#cppcore #memory
❤🔥40👍26🔥9❤8⚡1
Еще один способ сделать new небросающим
#опытным
Дело в том, что new - не совсем ответственнен за поведение при недостатке памяти. По плюсовой традиции тут можно кастомизировать почти все. Встречайте: std::new_handler.
Это такой typedef'чик:
И алиас для функций, которые отвечают за обработку ситуации нехватки памяти. И вызываются они аллоцирующими функциями operator new и operator new[].
Чтобы установить такой хэдлер используется функция std::set_new_handler:
Она делает new_p новой глобальной функцией нового обработчика и возвращает ранее установленный обработчик.
Предполагаемое назначение хэндлера - одна из трех вещей:
1️⃣ Сделать больше памяти доступной. (За гранью фантастики)
2️⃣ Залогировать проблему и завершить программу (например, вызовом std::terminate) +. Если вам плевать на graceful shutdown, то ок.
3️⃣ Кинуть исключение типа std::bad_alloc или его отпрысков с какой-нибудь кастомной надписью и/или залогировать проблему.
Раз std::new_handler - алиас на указатель функции, то что будет, если мы передадим nullptr в качестве хэндлера?
На самом деле это и делается по умолчанию. При старте программы nullptr выставляется в качестве хэндлера. При невозможности выделить память operator new вызывает std::get_new_handler. Если указатель на функцию нулевой, то в этом случае new ведет себя дефолтно - просто кидает исключение std::bad_alloc. В ином случае вызывает хэндлер.
С обработчиками есть один нюанс.
Если обработчик успешно заканчивает работу(то есть из него не вызван terminate или не брошено исключение), то operator new повторяет ранее неудачную попытку выделения и снова вызывает обработчик, если выделение снова не удается. Чтобы закончить цикл, new-handler может вызвать std::set_new_handler(nullptr): если после неудачной попытки new обнаружит, что std::get_new_handler возвращает нулевое значение указателя и выбросит std::bad_alloc.
Примерно так это работает:
Пользуйтесь фичей, чтобы оставлять на проде девопсерам сообщение: "А че так мало памяти в конфиге пода прописано, мм?"
Спасибо @Nikseas_314 за идею для поста)
Customize your tools. Stay cool.
#memory #cppcore
#опытным
Дело в том, что new - не совсем ответственнен за поведение при недостатке памяти. По плюсовой традиции тут можно кастомизировать почти все. Встречайте: std::new_handler.
Это такой typedef'чик:
typedef void (*new_handler)();
И алиас для функций, которые отвечают за обработку ситуации нехватки памяти. И вызываются они аллоцирующими функциями operator new и operator new[].
Чтобы установить такой хэдлер используется функция std::set_new_handler:
std::new_handler set_new_handler(std::new_handler new_p) noexcept;
Она делает new_p новой глобальной функцией нового обработчика и возвращает ранее установленный обработчик.
Предполагаемое назначение хэндлера - одна из трех вещей:
1️⃣ Сделать больше памяти доступной. (За гранью фантастики)
2️⃣ Залогировать проблему и завершить программу (например, вызовом std::terminate) +. Если вам плевать на graceful shutdown, то ок.
3️⃣ Кинуть исключение типа std::bad_alloc или его отпрысков с какой-нибудь кастомной надписью и/или залогировать проблему.
Раз std::new_handler - алиас на указатель функции, то что будет, если мы передадим nullptr в качестве хэндлера?
На самом деле это и делается по умолчанию. При старте программы nullptr выставляется в качестве хэндлера. При невозможности выделить память operator new вызывает std::get_new_handler. Если указатель на функцию нулевой, то в этом случае new ведет себя дефолтно - просто кидает исключение std::bad_alloc. В ином случае вызывает хэндлер.
С обработчиками есть один нюанс.
Если обработчик успешно заканчивает работу(то есть из него не вызван terminate или не брошено исключение), то operator new повторяет ранее неудачную попытку выделения и снова вызывает обработчик, если выделение снова не удается. Чтобы закончить цикл, new-handler может вызвать std::set_new_handler(nullptr): если после неудачной попытки new обнаружит, что std::get_new_handler возвращает нулевое значение указателя и выбросит std::bad_alloc.
Примерно так это работает:
void handler()
{
std::cout << "Memory allocation failed, terminating\n";
std::set_new_handler(nullptr);
}
int main()
{
std::set_new_handler(handler);
try
{
while (true)
{
new int [1000'000'000ul] ();
}
}
catch (const std::bad_alloc& e)
{
std::cout << e.what() << '\n';
}
}
Пользуйтесь фичей, чтобы оставлять на проде девопсерам сообщение: "А че так мало памяти в конфиге пода прописано, мм?"
Спасибо @Nikseas_314 за идею для поста)
Customize your tools. Stay cool.
#memory #cppcore
👍37😁16❤🔥6❤4🔥2
Почему мы везде не используем nothrow new?
#опытным
В прошлый раз мы обсудили, что существует форма оператора new, которая не возбуждает исключений, а вместо этого при ошибке возвращает нулевой указатель. Однако я почему-то уверен, что большинство из вас впервые увидели эту форму. Почему же ее практически нигде не используют?
1️⃣ В современных плюсах вообще не часто можно увидеть прямой вызов new. Контейнеры и функции helper'ы std::make_* инкапсулируют в себе аллокации. Внутри них вызывается обычный бросающий new. Только в очень специфических кейсах явный вызов new оправдан. Поэтому пул примеров в принципе очень небольшой.
2️⃣ Представьте, что у вас закончилась память и nothrow new вернул вас nullptr. Можете ли вы локально обработать ошибку недостатка памяти? 99.9%, что нет. Поэтому вы будете вести эту ошибку по всему стеку вызовов до того места, где ее возможно обработать. То есть весь проект должен быть построен с учетом возможности возврата ошибки и постоянной проверкой этих ошибок.
3️⃣ И все же есть те люди, которых устраивает такая форма проекта с возвратом ошибки из функции и постоянной ее проверкой. Но если немного подумать, то выяснится, что очень часто ошибку недостатка памяти вы примерно никак не сможете обработать, кроме как напишите об этом в лог и завершите приложение. То есть, шо словите вы std::bad_alloc, шо просто напишите об этом в лог - разница не большая.
Раз разница небольшая, мы можем обработать ошибку только на определенном слое приложения, а исключения предоставляют возможность централизованной их обработки, то давайте только там и поставим эту обработку. В одном единственном месте. Скорее всего это будет где-то в функции main или в главной функции потока.
Сейчас может начаться холивар "исключения vs объекты ошибок". Но вряд ли можно отрицать, что работать с bad_alloc исключением банально проще, чем обрабатывать nullptr. Именно поэтому вы скорее всего вообще не увидите nothrow new.
Handle problems easily. Stay cool.
#cppcore
#опытным
В прошлый раз мы обсудили, что существует форма оператора new, которая не возбуждает исключений, а вместо этого при ошибке возвращает нулевой указатель. Однако я почему-то уверен, что большинство из вас впервые увидели эту форму. Почему же ее практически нигде не используют?
1️⃣ В современных плюсах вообще не часто можно увидеть прямой вызов new. Контейнеры и функции helper'ы std::make_* инкапсулируют в себе аллокации. Внутри них вызывается обычный бросающий new. Только в очень специфических кейсах явный вызов new оправдан. Поэтому пул примеров в принципе очень небольшой.
2️⃣ Представьте, что у вас закончилась память и nothrow new вернул вас nullptr. Можете ли вы локально обработать ошибку недостатка памяти? 99.9%, что нет. Поэтому вы будете вести эту ошибку по всему стеку вызовов до того места, где ее возможно обработать. То есть весь проект должен быть построен с учетом возможности возврата ошибки и постоянной проверкой этих ошибок.
3️⃣ И все же есть те люди, которых устраивает такая форма проекта с возвратом ошибки из функции и постоянной ее проверкой. Но если немного подумать, то выяснится, что очень часто ошибку недостатка памяти вы примерно никак не сможете обработать, кроме как напишите об этом в лог и завершите приложение. То есть, шо словите вы std::bad_alloc, шо просто напишите об этом в лог - разница не большая.
Раз разница небольшая, мы можем обработать ошибку только на определенном слое приложения, а исключения предоставляют возможность централизованной их обработки, то давайте только там и поставим эту обработку. В одном единственном месте. Скорее всего это будет где-то в функции main или в главной функции потока.
Сейчас может начаться холивар "исключения vs объекты ошибок". Но вряд ли можно отрицать, что работать с bad_alloc исключением банально проще, чем обрабатывать nullptr. Именно поэтому вы скорее всего вообще не увидите nothrow new.
Handle problems easily. Stay cool.
#cppcore
❤🔥17👍14❤4😁4🔥3
Ограничения в конструировании POD типов
#опытным
Довольно часто приходится работать с plain old data типами. Они не имеют никаких специальных методов и конструкторов, это просто структуры с полями. Например:
Создают объекты таких типов с помощью синтаксиса универсальной инициализации через фигурные скобки {}:
При этом через круглые скобки создать объект такого класса нельзя.
С повсеместным использованием универсальной инициализации с точки зрения код стайла создание таких объектов даже не выбивается из общего кода.
Но все-таки у этого есть проблемы.
Часто такие структурки хочется хранить в каком-нибудь векторе. Для добавления в вектор можно использовать push_back и emplace_back. Использование emplace_back выгоднее по перформансу, поэтому нужно обычно использовать именно этот метод.
Но с POD типами он работает хреново.
Вот так вы написать не можете. emplace_back под капотом использует создание объекта с помощью new именно через круглые скобки. А как мы помним, так инициализировать POD типы нельзя.
Поэтому приходится в этих случаях явно конструировать объект и вызывать мув-конструктор:
что убивает преимущества emplace_back перед push_back. Либо можно использовать непросредственно push_back.
Та же проблемы и в использовании функций std::make_*. Под капотом они тоже используют new с круглыми скобками и просто невозможно нормально использовать эти функции. Приходится явно вызывать new с фигурными скобками, что усугубляет проблему:
Конечно кейс с emplace_back намного чаще встречается в практике. POD типам не нужны фабличные методы и, обычно, контроль времени жизни.
В общем, не жизнь, а страдания.
Однако есть свет в конце тоннеля! Но об этом в следующий раз.
See the light. Stay cool.
#cppcore
#опытным
Довольно часто приходится работать с plain old data типами. Они не имеют никаких специальных методов и конструкторов, это просто структуры с полями. Например:
struct Point {
int x;
int y;
int z;
}Создают объекты таких типов с помощью синтаксиса универсальной инициализации через фигурные скобки {}:
Point p{1, 2, 3};
Point p(1, 2, 3); // WrongПри этом через круглые скобки создать объект такого класса нельзя.
С повсеместным использованием универсальной инициализации с точки зрения код стайла создание таких объектов даже не выбивается из общего кода.
Но все-таки у этого есть проблемы.
Часто такие структурки хочется хранить в каком-нибудь векторе. Для добавления в вектор можно использовать push_back и emplace_back. Использование emplace_back выгоднее по перформансу, поэтому нужно обычно использовать именно этот метод.
Но с POD типами он работает хреново.
std::vector<Point> vec;
vec.emplace_back(1, 2, 3); // Error!
Вот так вы написать не можете. emplace_back под капотом использует создание объекта с помощью new именно через круглые скобки. А как мы помним, так инициализировать POD типы нельзя.
Поэтому приходится в этих случаях явно конструировать объект и вызывать мув-конструктор:
vec.emplace_back(Point{1, 2, 3});что убивает преимущества emplace_back перед push_back. Либо можно использовать непросредственно push_back.
Та же проблемы и в использовании функций std::make_*. Под капотом они тоже используют new с круглыми скобками и просто невозможно нормально использовать эти функции. Приходится явно вызывать new с фигурными скобками, что усугубляет проблему:
std::unique_ptr<Point>(new Point{1, 2, 3}); Конечно кейс с emplace_back намного чаще встречается в практике. POD типам не нужны фабличные методы и, обычно, контроль времени жизни.
В общем, не жизнь, а страдания.
Однако есть свет в конце тоннеля! Но об этом в следующий раз.
See the light. Stay cool.
#cppcore
❤25👍10⚡4🔥4😁3
Фиксим проблему с конструированием POD типов
#опытным
В прошлом посте говорили о том, что тяжело POD типам работать с функциями, которые внутри себя вызывают new. Клятые скобки!
Это очевидные недоработки стандарта. Которые взяли и пофиксили в С++20!
Теперь объекты POD типов можно создавать как через фигурные скобки, так и через круглые. Хотя небольшая разница есть. Но это уже супердетали:
Теперь можно использовать все преимущества метода emplace_back и писать такой код:
Вроде мелочь, а раньше это выбивало из колеи. Правило almost always use emplace_back здесь не работало.
Давайте добавлю еще чуть больше контекста(и своей боли) про emplace_back vs push_back.
Что было до с++20.
push_back спокойно переваривал следующий код:
Он принимает аргументом уже сконструированный объект нужного типа. И компилятору достаточно знаний о типе вектора и фирурных скобок, чтобы понять, как конструируется объект.
Как видите здесь нет явного указания типа. И это работало до 20-х плюсов и было причиной делать исключения для правила с emplace_back'ом. Сравните:
Больше букав! А если название структуры длинное? Вот вот.
Тут либо нужно было отходить от правила везде использования emplace_back, либо постоянно вписывать конструктор в аргументы вызова emplace_back, либо определять совершенно тривиальные и от этого не несущие полезной логики конструкторы.
Благо теперь наши пальцы сильнее защищены от стачивания, что не может не радовать!
Кстати, с С++20 и сам термин POD стал помечаться устаревшим, теперь его заменили более тонкие типы TrivialType, ScalarType, и StandardLayoutType. Ну и функции std::make_* тоже работают как надо с простыми структурами.
Enjoy small things. Stay cool.
#cppcore #cpp20
#опытным
В прошлом посте говорили о том, что тяжело POD типам работать с функциями, которые внутри себя вызывают new. Клятые скобки!
Это очевидные недоработки стандарта. Которые взяли и пофиксили в С++20!
Теперь объекты POD типов можно создавать как через фигурные скобки, так и через круглые. Хотя небольшая разница есть. Но это уже супердетали:
struct A {
int a;
int&& r;
};
int f();
int n = 10;
A a1{1, f()}; // OK, lifetime is extended
A a2(1, f()); // well-formed, but dangling reference
A a3{1.0, 1}; // error: narrowing conversion
A a4(1.0, 1); // well-formed, but dangling reference
A a5(1.0, std::move(n)); // OKТеперь можно использовать все преимущества метода emplace_back и писать такой код:
std::vector<Point> vec;
vec.emplace_back(1, 2, 3);
Вроде мелочь, а раньше это выбивало из колеи. Правило almost always use emplace_back здесь не работало.
Давайте добавлю еще чуть больше контекста(и своей боли) про emplace_back vs push_back.
Что было до с++20.
push_back спокойно переваривал следующий код:
std::vector<Point> vec;
vec.push_back({1, 2, 3});
Он принимает аргументом уже сконструированный объект нужного типа. И компилятору достаточно знаний о типе вектора и фирурных скобок, чтобы понять, как конструируется объект.
Как видите здесь нет явного указания типа. И это работало до 20-х плюсов и было причиной делать исключения для правила с emplace_back'ом. Сравните:
std::vector<Point> vec;
vec.push_back({1, 2, 3});
vec.emplace_back(Point{1, 2, 3});
Больше букав! А если название структуры длинное? Вот вот.
Тут либо нужно было отходить от правила везде использования emplace_back, либо постоянно вписывать конструктор в аргументы вызова emplace_back, либо определять совершенно тривиальные и от этого не несущие полезной логики конструкторы.
Благо теперь наши пальцы сильнее защищены от стачивания, что не может не радовать!
Кстати, с С++20 и сам термин POD стал помечаться устаревшим, теперь его заменили более тонкие типы TrivialType, ScalarType, и StandardLayoutType. Ну и функции std::make_* тоже работают как надо с простыми структурами.
Enjoy small things. Stay cool.
#cppcore #cpp20
❤27👍15🔥14❤🔥2
emplace_back vs push_back
#новичкам
Раз уж такая масленица пошла, расскажу про весь сыр-бор с методами вектора(да и не только вектора).
В последовательные контейнеры можно запихнуть данные в конец двумя способами: метод push_back и метод emplace_back.
По сигнатуре видно, что они предназначены немного для разного.
Начнем со сложного. emplace_back принимает пакет параметров. Эти параметры предполагаются как аргументы конструктора хранимого типа T. Реализован он примерно так:
Если надо, то расширяемся и делаем placement new на участке памяти для нового объекта, попутно используя perfect forwarding для передачи аргументов в конструктор. Вот тут кстати те самые круглые скобки используются, которые не давали pod типам нормально конструироваться.
push_back принимает ссылку на уже готовый объект. То есть объект должен быть создан до входа в метод. И на основе этого значения уже конструируется объект в контейнере. В простейшем случае push_back вызывает внутри себя emplace_back:
Чтобы вызвать пуш бэк нужно вызвать 2 конструктора: от аргументов и copy|move. Для emplace_back же нужен только один конструктор - от аргументов.
То есть emplace_back банально эффективнее, чем push_back. Для случаев, когда мы почему-то не можем создать объект внутри emplace_back(POD типы и < С++20) мы его создаем снаружи и копируем/муваем внутрь. Тогда эффективности двух методов одинаковая.
Получается, что emplace_back в любом случае не менее эффективнее, чем push_back. Именно поэтому нужно всегда предпочитать использовать emplace_back.
Be just better. Stay cool.
#STL #memory
#новичкам
Раз уж такая масленица пошла, расскажу про весь сыр-бор с методами вектора(да и не только вектора).
В последовательные контейнеры можно запихнуть данные в конец двумя способами: метод push_back и метод emplace_back.
template< class... Args >
reference emplace_back( Args&&... args ); // returns ref to created element
void push_back( const T& value );
void push_back( T&& value );
По сигнатуре видно, что они предназначены немного для разного.
Начнем со сложного. emplace_back принимает пакет параметров. Эти параметры предполагаются как аргументы конструктора хранимого типа T. Реализован он примерно так:
template <typename... Args>
reference emplace_back(Args&&... args) {
if (size == capacity) grow();
return *new (start + size++) T(std::forward<Args>(args)...);
}
Если надо, то расширяемся и делаем placement new на участке памяти для нового объекта, попутно используя perfect forwarding для передачи аргументов в конструктор. Вот тут кстати те самые круглые скобки используются, которые не давали pod типам нормально конструироваться.
push_back принимает ссылку на уже готовый объект. То есть объект должен быть создан до входа в метод. И на основе этого значения уже конструируется объект в контейнере. В простейшем случае push_back вызывает внутри себя emplace_back:
void push_back(T&& value) {
emplace_back(std::move(value));
}Чтобы вызвать пуш бэк нужно вызвать 2 конструктора: от аргументов и copy|move. Для emplace_back же нужен только один конструктор - от аргументов.
То есть emplace_back банально эффективнее, чем push_back. Для случаев, когда мы почему-то не можем создать объект внутри emplace_back(POD типы и < С++20) мы его создаем снаружи и копируем/муваем внутрь. Тогда эффективности двух методов одинаковая.
Получается, что emplace_back в любом случае не менее эффективнее, чем push_back. Именно поэтому нужно всегда предпочитать использовать emplace_back.
Be just better. Stay cool.
#STL #memory
🔥30👍13❤6😁3🤔3⚡2
Когда мы вынуждены явно использовать new
#опытным
Сырые указатели - фуфуфу, бееее. Это не вкусно, мы такое не едим. new expression возвращает сырой указатель на объект. Соотвественно, мы должны максимально избегать явного использования new. У нас все-таки умные указатели и функции std::make_* довольно давно завезли.
Однако все-таки есть кейсы, когда мы просто вынуждены использовать new явно:
👉🏿 std::make_unique не может в кастомные делитеры. Если хотите создать уникальный указатель со своим удалителем - придется использовать new.
👉🏿 Приватный конструктор у класса. Странно вообще пытаться создать объект такого класса, но не торопитесь. Приватный конструктор может быть нужен, чтобы оставить только один легальный способ создания объекта - фабричную функцию Create. Она возвращает уникальный указатель на объект и обычно является статическим членом класса. Функция Create имеет доступ к приватным методам, поэтому может вызвать конструктор. Но вот std::make_unique ничего не знает о приватных методах класса и не сможет создать объект. Придется использовать new.
👉🏿 Жизнь без 20-го стандарта. До 20-го стандарта вы не могли создать объект POD класса без указания фигурных скобок. Но именно так и делает std::make_unique.
То есть вот так нельзя делать в С++17:
Но можно в С++20. Так что тем, кто необновился, придется использовать new.
В целом, все. Если что забыл - накидайте в комменты.
Но помимо этого, администрация этого канала не рекомендует в домашних и рабочих условиях явно вызывать new. Это может привести к потери конечности(отстрелу ноги).
Stay safe. Stay cool.
#cppcore #memory #cpp20 #cpp17
#опытным
Сырые указатели - фуфуфу, бееее. Это не вкусно, мы такое не едим. new expression возвращает сырой указатель на объект. Соотвественно, мы должны максимально избегать явного использования new. У нас все-таки умные указатели и функции std::make_* довольно давно завезли.
Однако все-таки есть кейсы, когда мы просто вынуждены использовать new явно:
👉🏿 std::make_unique не может в кастомные делитеры. Если хотите создать уникальный указатель со своим удалителем - придется использовать new.
auto ptr = std::unique_ptr<int, void()(int*)>(new int(42), [](int* p) {
delete p;
std::cout << "Custom deleter called!\n";
});👉🏿 Приватный конструктор у класса. Странно вообще пытаться создать объект такого класса, но не торопитесь. Приватный конструктор может быть нужен, чтобы оставить только один легальный способ создания объекта - фабричную функцию Create. Она возвращает уникальный указатель на объект и обычно является статическим членом класса. Функция Create имеет доступ к приватным методам, поэтому может вызвать конструктор. Но вот std::make_unique ничего не знает о приватных методах класса и не сможет создать объект. Придется использовать new.
struct Class {
static std::unique_ptr<Class> Create() {
// return std::make_unique<Class>(); // It will fail.
return std::unique_ptr<Class>(new Class);
}
private:
Class() {}
};👉🏿 Жизнь без 20-го стандарта. До 20-го стандарта вы не могли создать объект POD класса без указания фигурных скобок. Но именно так и делает std::make_unique.
То есть вот так нельзя делать в С++17:
struct MyStruct {
int a, b, c;
};
auto ptr = std::make_unique<MyStruct>(1, 2, 3); // Will fail C++17
auto ptr = std::unique_ptr<MyStruct>(new MyStruct{1, 2, 3}); // NormНо можно в С++20. Так что тем, кто необновился, придется использовать new.
В целом, все. Если что забыл - накидайте в комменты.
Но помимо этого, администрация этого канала не рекомендует в домашних и рабочих условиях явно вызывать new. Это может привести к потери конечности(отстрелу ноги).
Stay safe. Stay cool.
#cppcore #memory #cpp20 #cpp17
👍24❤7🔥2😁2⚡1
Проблемы С-style массивов
#опытным
В наследство от языка С С++ достались статические массивы. Так называемые С-style массивы. Это проверенные средства языка, успешно решающие свои задачи. Но у них есть серьезные недостатки, которые в основном связаны с низкоуровневостью этого инструмента.
Давайте кратко повторим, что такое C-style массив.
Это непрерывная последовательность элементов одного типа и память под них выделяется на стеке(или реже в статический области, если массив глобальный) и автоматически освобождается при выходе из функции.
Определяется сишный массив вот так:
Размер памяти, занимаемый массивом равняется количеству его элементов помноженному на размер типа данных:
Соответственно, для получения количества элементов массива, нужно поделить sizeof от массива на размер типа данных, которые он хранит.
В чем же его недостатки?
❗️ Массивы нельзя сравнивать напрямую, а только поэлементно. Напрямую сравниваются указатели на первый элемент.
❗️ Мимикрирование под массивы разрешает странную семантику с условиями и арифметическими операциями.
❗️В С разрешены [массивы переменной длины](https://t.iss.one/grokaemcpp/56) на уровне стандарта. И синтаксис у них ровно такой же, как и у статических массивов, только при его создании размер указывается не константой, а переменной. В С++ это не стандартная фича, а расширения компилятора. То есть нельзя писать кроссплатформенный код с использованием массивов переменной длины. Но за счет идентичного синтаксиса очень легко спутать один вид массива с другим и похерить переменосимость.
❗️ От синтаксиса сочетания функций и массивов хочется вырвать себе глаза, закрыть компьютер и уйти жить в лес:
❗️Массив не инкапсулирует в себе свой размер. Его нужно всегда вычислять, как мы говорили в начале.
❗️Из-за сложности синтаксиса, вы скорее всего захотите обрабатывать массивы с помощью функций с похожей сигнатурой:
Это потенциально может привести к доступу за границы выделенной области, так как функция foo ничего не знает про то, какой реальный размер имеет область памяти, на которую указывает
В общем, сишные массивы - это не объекты и не обладают преимуществами ООП и универсальной семантики для объектов в С++.
Поэтому стандартная библиотека предоставляет нам инструмент, который решает все проблемы C-style массивов. Это контейнер std::array. О нем мы поговорим в следующий раз.
Upgrade your tools. Stay cool.
#cppcore #goodoldc
#опытным
В наследство от языка С С++ достались статические массивы. Так называемые С-style массивы. Это проверенные средства языка, успешно решающие свои задачи. Но у них есть серьезные недостатки, которые в основном связаны с низкоуровневостью этого инструмента.
Давайте кратко повторим, что такое C-style массив.
Это непрерывная последовательность элементов одного типа и память под них выделяется на стеке(или реже в статический области, если массив глобальный) и автоматически освобождается при выходе из функции.
Определяется сишный массив вот так:
// создаем массив на 5 элементов,
// которые базово инициализируются мусором
int arr1[5];
// создаем массив на 5 элементов,
// которые инициализируются нулями
int arr1[5]{};
// создаем массив и предоставляем набор элементов, с помощью
// которых компилятор вычисляет длину массива и инициализирует элементы
int arr2[] = {1, 2, 3, 4, 5};
Размер памяти, занимаемый массивом равняется количеству его элементов помноженному на размер типа данных:
constexpr size_t array_size = 5;
int arr[array_size];
sizeof(arr) == array_size * sizeof(int); // true
Соответственно, для получения количества элементов массива, нужно поделить sizeof от массива на размер типа данных, которые он хранит.
auto array_size = sizeof(arr) / sizeof(Type);
В чем же его недостатки?
❗️ Массивы нельзя сравнивать напрямую, а только поэлементно. Напрямую сравниваются указатели на первый элемент.
int arr1[] = {0, 1, 2, 3};
int arr2[] = {0, 1, 2, 3};
// ложь так как сраниваются указатели,
// а они разные для разных объектов
arr1 == arr2; ❗️ Мимикрирование под массивы разрешает странную семантику с условиями и арифметическими операциями.
// создаем пустую строку в виде массива
char arr[] = "";
// условие будет всегда true, хотя мы создали пустую строку
if (arr);
// разрешается, но зачем? что значит прибавить к массиву число?
arr + 1;
❗️В С разрешены [массивы переменной длины](https://t.iss.one/grokaemcpp/56) на уровне стандарта. И синтаксис у них ровно такой же, как и у статических массивов, только при его создании размер указывается не константой, а переменной. В С++ это не стандартная фича, а расширения компилятора. То есть нельзя писать кроссплатформенный код с использованием массивов переменной длины. Но за счет идентичного синтаксиса очень легко спутать один вид массива с другим и похерить переменосимость.
❗️ От синтаксиса сочетания функций и массивов хочется вырвать себе глаза, закрыть компьютер и уйти жить в лес:
int foo(int arr[4]);
// На самом деле такая сигнатура полностью эквивалентна int foo(int * arr),
// что позволяет принимать в функцию массив любой длины и указатели.
// В С++ нет синтаксиса приема массива по значению.
void foo(int (&arr)[4]); // зато есть синтаксис приема массива по ссылке
// Нормального синтаксиса для возврата массива из функции также не завезли.
// Вот воркэраунды.
int get_array()[10];
auto get_array() -> int[10];
❗️Массив не инкапсулирует в себе свой размер. Его нужно всегда вычислять, как мы говорили в начале.
❗️Из-за сложности синтаксиса, вы скорее всего захотите обрабатывать массивы с помощью функций с похожей сигнатурой:
void foo(int * p, size_t size);
Это потенциально может привести к доступу за границы выделенной области, так как функция foo ничего не знает про то, какой реальный размер имеет область памяти, на которую указывает
p. Она должна доверять программисту и переданному значению size. А программисту верить - себя не уважать.В общем, сишные массивы - это не объекты и не обладают преимуществами ООП и универсальной семантики для объектов в С++.
Поэтому стандартная библиотека предоставляет нам инструмент, который решает все проблемы C-style массивов. Это контейнер std::array. О нем мы поговорим в следующий раз.
Upgrade your tools. Stay cool.
#cppcore #goodoldc
🔥37👍15❤🔥3❤2😁2⚡1
std::array
#новичкам
На самом деле, это очень-очень тонкая обертка над сишными массивами. Вот несколько упрощенная реализация, которая тем не менее полностью передает смысл и необходимые особенности.
За счет использования шаблоного типа нижележащего массива std::array может работать с любыми встроенными и кастомными типами.
А за счет нетипового шаблонного аргумента N, std::array знает количество элементов, которое в нем находится, еще на этапе компиляции!. И не нужно ничего вычислять! Достаточно вызвать метод size(), который буквально constexpr.
Обычно удобство абстракций идет вместе с платой за это удобство. Но это не тот случай. За счет того, что все методы std::array буквально занимают одну строчку, компилятору очень удобно инлайнить их код в caller'ов. Это приводит к тому, что низкоуровневый ассемблерный код при работе с C-style массивами и std::array практически всегда идентичен.
std::array не мимикрирует ни под какой другой тип, так как это кастомный класс. Внутри себя он также инкапсулирует все необходимые операторы сравнения. В операциях с ним нет никакой путаницы, потому что они явно определены конкретно для этого класса. Его можно спокойно принимать в функцию по ссылке и по значению, а также указывать в качестве возвращаемого значения. И все это с привычной семантикой.
Если мы создаем массив в локальной области функции(99% случаев), то элементы std::array располагаются непрерывно на стеке. И размер std::array равен размеру C-style массива с одинаковым количеством элементов и их типом.
Итак. Выходит, что std::array идентичен сишному массиву по внутреннему устройству и произодительности, да еще и решает все проблемы неумелого использования последнего. Идеальный высокоуровневый инструмент!
Так что std::array должен быть первым выбором в случае необходимости создания массива с длиной, известной на этапе компиляции. У PVS-Studio есть прекрасная статья на этот счет.
Fix your flaws. Stay cool.
#STL #cppcore
#новичкам
На самом деле, это очень-очень тонкая обертка над сишными массивами. Вот несколько упрощенная реализация, которая тем не менее полностью передает смысл и необходимые особенности.
template<typename T, size_t N>
struct array
{
T& operator[](size_t index) {
return _data[index];
}
T& front() {
return _data[0];
}
T& back() {
return _data[N-1];
}
T* data() {
return _data;
}
constexpr size_t size() const {
return N;
}
constexpr bool empty() const {
return N == 0;
}
// еще const версии перечисленных методов и некоторые другие методы и алиасы типов
T _data[N];
};
За счет использования шаблоного типа нижележащего массива std::array может работать с любыми встроенными и кастомными типами.
А за счет нетипового шаблонного аргумента N, std::array знает количество элементов, которое в нем находится, еще на этапе компиляции!. И не нужно ничего вычислять! Достаточно вызвать метод size(), который буквально constexpr.
std::array arr{1, 2, 3};
static_assert(arr.size() == 3); // здесь не упадемОбычно удобство абстракций идет вместе с платой за это удобство. Но это не тот случай. За счет того, что все методы std::array буквально занимают одну строчку, компилятору очень удобно инлайнить их код в caller'ов. Это приводит к тому, что низкоуровневый ассемблерный код при работе с C-style массивами и std::array практически всегда идентичен.
std::array не мимикрирует ни под какой другой тип, так как это кастомный класс. Внутри себя он также инкапсулирует все необходимые операторы сравнения. В операциях с ним нет никакой путаницы, потому что они явно определены конкретно для этого класса. Его можно спокойно принимать в функцию по ссылке и по значению, а также указывать в качестве возвращаемого значения. И все это с привычной семантикой.
template<typename T, size_t N>
std::array<T, N> double_elements(std::array<T, N>& array) {
std::array<T, N> result = array;
for (auto& elem: result)
elem = elem * 2;
return result;
}
Если мы создаем массив в локальной области функции(99% случаев), то элементы std::array располагаются непрерывно на стеке. И размер std::array равен размеру C-style массива с одинаковым количеством элементов и их типом.
int c_arr[N];
std::array<int, N> cpp_arr;
sizeof(cpp_arr) == cpp_arr.size() * sizeof(int) ==
sizeof(c_arr) == N * sizeof(int) == std::size(c_arr) * sizeof(int);
Итак. Выходит, что std::array идентичен сишному массиву по внутреннему устройству и произодительности, да еще и решает все проблемы неумелого использования последнего. Идеальный высокоуровневый инструмент!
Так что std::array должен быть первым выбором в случае необходимости создания массива с длиной, известной на этапе компиляции. У PVS-Studio есть прекрасная статья на этот счет.
Fix your flaws. Stay cool.
#STL #cppcore
Хабр
std::array в С++ не медленнее массива в С
Или почему не нужно бояться того, что удобно работает. Стойте! Уберите руки от клавиатуры, дайте человеку сказать! У этой статьи есть обоснованные причины и благая цель! В прошлой моей статье о...
🔥32👍16❤5
ref-qualified методы
#опытным
В С++ можно довольно интересными способами перегружать методы класса. Один из самых малоизвестных и малоиспользуемых - помечать методы квалификатором ссылочности.
Чтобы было понятнее. Примерно все знают, что бывают константные и неконстантные методы.
Константные объекты могут вызывать только константные методы. Поэтому мы можем перегрузить метод класса, чтобы он мог работать с константными объектами.
В примере видно что у константного объекта вызывается константная перегрузка.
По аналогии с cv-квалификаторами методов начиная с С++11 существуют ref-квалификаторы. Мы можем перегрузить метод так, чтобы он мог раздельно обрабатывать левые и правые ссылки.
Обратим внимание на сигнатуру методов. Метки ссылочных квалификаторов ожидаемо принимают форму одного и двух амперсандов, по аналогии с типами данных левых и правых сслылок соотвественно. Располагаются они после скобок с аргументами метода.
Работают они примерно также, как вы и ожидаете. lvalue-ref перегрузка вызывается на именованном объекте, rvalue-ref перегрузка - на временном.
Зачем это придумано?
Здесь на самом деле большие параллели с cv-квалификацией методов. Допустим, у вас класс - это какая-то коллекция. И вы хотите давать пользователям доступ к элементам этой коллекции через оператор[]. Для неконстантных объектов удобно возвращать ссылку. А вот для константных возвращение ссылки - потенциальное нарушение неизменяемости объекта. Поэтому в таких случаях константный оператор может возвращать элемент по значению или по константной ссылке.
Также и с ссылочностью. В каких-то случаях оптимально или просто необходимо использовать для правых ссылок иную логику метода.
Подробнее об этом чуде-юде будем разбираться в следующих постах.
Stay flexible. Stay cool.
#cpp11 #design
#опытным
В С++ можно довольно интересными способами перегружать методы класса. Один из самых малоизвестных и малоиспользуемых - помечать методы квалификатором ссылочности.
Чтобы было понятнее. Примерно все знают, что бывают константные и неконстантные методы.
struct SomeClass {
void foo() {std::cout << "Non-const member function" << std::endl;}
void foo() const {std::cout << "Const member function" << std::endl;}
};
SomeClass nonconst_obj;
const SomeClass const_obj;
nonconst_obj.foo();
const_obj.foo();
// OUTPUT
// Non-const member function
// Const member functionКонстантные объекты могут вызывать только константные методы. Поэтому мы можем перегрузить метод класса, чтобы он мог работать с константными объектами.
В примере видно что у константного объекта вызывается константная перегрузка.
По аналогии с cv-квалификаторами методов начиная с С++11 существуют ref-квалификаторы. Мы можем перегрузить метод так, чтобы он мог раздельно обрабатывать левые и правые ссылки.
struct SomeClass {
void foo() & {std::cout << "Call on lvalue reference" << std::endl;}
void foo() && {std::cout << "Call on rvalue reference" << std::endl;}
};
SomeClass lvalue;
lvalue.foo();
SomeClass{}.foo();
// OUTPUT
// Call on lvalue reference
// Call on rvalue referenceОбратим внимание на сигнатуру методов. Метки ссылочных квалификаторов ожидаемо принимают форму одного и двух амперсандов, по аналогии с типами данных левых и правых сслылок соотвественно. Располагаются они после скобок с аргументами метода.
Работают они примерно также, как вы и ожидаете. lvalue-ref перегрузка вызывается на именованном объекте, rvalue-ref перегрузка - на временном.
Зачем это придумано?
Здесь на самом деле большие параллели с cv-квалификацией методов. Допустим, у вас класс - это какая-то коллекция. И вы хотите давать пользователям доступ к элементам этой коллекции через оператор[]. Для неконстантных объектов удобно возвращать ссылку. А вот для константных возвращение ссылки - потенциальное нарушение неизменяемости объекта. Поэтому в таких случаях константный оператор может возвращать элемент по значению или по константной ссылке.
Также и с ссылочностью. В каких-то случаях оптимально или просто необходимо использовать для правых ссылок иную логику метода.
Подробнее об этом чуде-юде будем разбираться в следующих постах.
Stay flexible. Stay cool.
#cpp11 #design
🔥29👍12❤4🤯3
Совмещаем ссылочные и cv квалификаторы методов
Далеко не всегда очевидно, какая именно функция является лучшим кандидатом для перегрузки в том или ином случае. Да, когда это только & или &&, то все довольно просто. Но что получается, когда мы добавим константность методам?
Компилятор будет выбирать подходящую перегрузку по определенному алгоритму.
Дело в том, что все правые ссылки могут каститься к const lvalue reference, а левые к правым - ни при каких обстоятельствах. Неконстантные типы могут каститься к константным. И никак наоборот.
Исходя из этих правил компилятор и разрешает перегрузки. Пометил методы из примера порядковыми номерами, чтобы потом было легче делать отсылки
В случае
Неконстантные типы могут приводиться к константным. Поэтому нам подходят и 1, и 2 методы. Однако для вызова 2 придется сделать шажок - добавить константности, а для вызова первого - ничего. Поэтому выбирается первая перегрузка.
В случае
Итого вывод получится такой:
Для rvalue ссылки нехитрыми рассуждениями можно прийти к правильному ответу о вызываемой перегрузке
Тут довольно все просто. Но самая жесть начинается, когда у нас нет какой-то перегрузки/перегрузок из полного набора. На следующей неделе забомбардирую вас мини-квизами на эту тему. Посмотрим, как хорошо вы шарите за overload resolution.
Choose the right way. Stay cool.
#cppcore
Далеко не всегда очевидно, какая именно функция является лучшим кандидатом для перегрузки в том или ином случае. Да, когда это только & или &&, то все довольно просто. Но что получается, когда мы добавим константность методам?
Компилятор будет выбирать подходящую перегрузку по определенному алгоритму.
struct SomeClass {
void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1
void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2
void foo() && {std::cout << "Call on rvalue reference" << std::endl;} //3
void foo() const && {std::cout << "Call on const rvalue reference" << std::endl;} //4
};Дело в том, что все правые ссылки могут каститься к const lvalue reference, а левые к правым - ни при каких обстоятельствах. Неконстантные типы могут каститься к константным. И никак наоборот.
Исходя из этих правил компилятор и разрешает перегрузки. Пометил методы из примера порядковыми номерами, чтобы потом было легче делать отсылки
SomeClass lvalue;
const SomeClass const_lvalue;
lvalue.foo();
const_lvalue.foo();
В случае
lvalue.foo() вызываем перегрузку для неконстантной левой ссылки. Левые ссылки не могут приводиться к правым. Поэтому методы под номерами 3 и 4 не подходят. Неконстантные типы могут приводиться к константным. Поэтому нам подходят и 1, и 2 методы. Однако для вызова 2 придется сделать шажок - добавить константности, а для вызова первого - ничего. Поэтому выбирается первая перегрузка.
В случае
const_lvalue.foo() вызываем перегрузку для константной левой ссылки. 3 и 4 также откидываем по тем же причинам. Однако в этот раз нам подходит лишь 2 перегрузка, так как константный тип не может быть приведен к неконстантному.Итого вывод получится такой:
Call on lvalue reference
Call on const lvalue reference
Для rvalue ссылки нехитрыми рассуждениями можно прийти к правильному ответу о вызываемой перегрузке
SomeClass{}.foo();
// OUTPUT
// Call on rvalue referenceТут довольно все просто. Но самая жесть начинается, когда у нас нет какой-то перегрузки/перегрузок из полного набора. На следующей неделе забомбардирую вас мини-квизами на эту тему. Посмотрим, как хорошо вы шарите за overload resolution.
Choose the right way. Stay cool.
#cppcore
❤17👍11🔥9❤🔥2⚡1
Всем привет!
Давайте проведем небольшой опрос, насколько опытные плюсовики у нас тут присутствуют. Так мы сможем более адекватно подстраивать контент под аудиторию.
Как вы оцениваете свой уровень владения С++?
Давайте проведем небольшой опрос, насколько опытные плюсовики у нас тут присутствуют. Так мы сможем более адекватно подстраивать контент под аудиторию.
Как вы оцениваете свой уровень владения С++?
Anonymous Poll
32%
С++ меня избивает. Beginner
15%
Синяки все еще есть, но я уже работаю. Junior
22%
Все вроде понимаю, но нихрена не понятно. Middle
17%
Виден свет в конце тоннеля. Middle+
11%
Написал себе нунчаки на С++. Senior
4%
Чемпион мира С++, народный артист России и Чечено-Ингушетии, человек признанный, авторитетный, мэтр
🤣57❤🔥8❤6⚡3😢1
Мини-квизы
Сейчас пойдет пачка мини-квизов на проверку того, как хорошо вы понимаете выбор ref-qualified перегрузок. Для меньшей запутанности я буду оставлять все перегрузки в тексте кода, но закомменчу ненужные в каждом случае. Также подключены все необходимые инклюды и компиляция происходит под 17-й стандарт.
В режиме опроса сложно указывать варианты с переносом строк. Поэтому на месте, где должен быть перенос буду ставить"\n".
Ответы выложу вечером.
Первый пошел:
Сейчас пойдет пачка мини-квизов на проверку того, как хорошо вы понимаете выбор ref-qualified перегрузок. Для меньшей запутанности я буду оставлять все перегрузки в тексте кода, но закомменчу ненужные в каждом случае. Также подключены все необходимые инклюды и компиляция происходит под 17-й стандарт.
В режиме опроса сложно указывать варианты с переносом строк. Поэтому на месте, где должен быть перенос буду ставить"\n".
Ответы выложу вечером.
Первый пошел:
struct SomeClass {
// void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1
void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2
void foo() && {std::cout << "Call on rvalue reference" << std::endl;} //3
void foo() const && {std::cout << "Call on const rvalue reference" << std::endl;} //4
};
int main() {
SomeClass lvalue;
lvalue.foo();
SomeClass{}.foo();
}🔥8❤3👍3⚡1
Какой результат попытки компиляции и запуска кода выше?
Anonymous Poll
19%
Ошибка компиляции
71%
Call on const lvalue reference\nCall on rvalue reference
10%
Call on const rvalue reference\nCall on const lvalue reference
🔥3❤1👍1
Второй пошел
struct SomeClass {
// void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1
void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2
// void foo() && {std::cout << "Call on rvalue reference" << std::endl;} //3
void foo() const && {std::cout << "Call on const rvalue reference" << std::endl;} //4
};
int main() {
SomeClass lvalue;
lvalue.foo();
std::move(lvalue).foo();
}🔥7⚡2
Каков результат попытки компиляции и запуска кода выше?
Anonymous Poll
16%
ошибка компиляции
65%
Call on const lvalue reference\nCall on const rvalue reference
19%
Call on const lvalue reference\nCall on const lvalue reference
🔥1