При вызове оператора new объекты инициализируются по умолчанию. То есть как для
Для заполнения динамического значения нулем, следует использовать круглые скобки, при этом круглые скобки можно использовать и с динамическими массивами (что создаст массив из нулей):
Интересно, что передать ненулевое значение для единичного объекта можно, а для массива нельзя:
int i; в результате будет мусор, так и для new int; в результате будет мусор. А вот например new string вызовет конструктор по умолчанию, то есть создаст пустую строку.Для заполнения динамического значения нулем, следует использовать круглые скобки, при этом круглые скобки можно использовать и с динамическими массивами (что создаст массив из нулей):
#include <iostream>
void fill_heap_with_garbage()
{
// повышаем вероятность мусора, а не 0
int* intp = new int(99);
delete intp;
int* arrp = new int[42];
for (size_t i=0; i<42; ++i) arrp[i] = i;
delete[] arrp;
}
int main()
{
fill_heap_with_garbage();
{
int* i1 = new int; // мусор
int* i2 = new int(); // 0
int* i3 = new int(42); // 42
std::cout << *i1 << " " << *i2 << " " << *i3
<< std::endl; // 1660576172 0 42
delete i1;
delete i2;
delete i3;
}
{
int* a1 = new int[42]; // мусор
int* a2 = new int[42](); // нули
std::cout << a1[10] << " " << a2[10]<< std::endl; // 10 0
delete[] a1;
delete[] a2;
}
return 0;
}
Интересно, что передать ненулевое значение для единичного объекта можно, а для массива нельзя:
new int(42); // можно, будет 42
new int[100](42) // нельзя
👍1
Определить массив нулевой длины в C++ нельзя, sizeof такого массива будет 0. Однако же, динамически выделить массив нулевой длины можно:
Такой указатель на пустой массив не будет нулевым, но он будет похож на указатель на элемент после конца массива.
Такой указатель можно сравнивать, добавить и вычесть 0, вычесть из себя. Поэтому цикл выше вполне валиден (он кстати не выполнится).
Конечно же, обращаться к значению такого указателя нельзя.
#include <iostream>
int main()
{
constexpr size_t size = 0;
int a1[0]; // нельзя
int * a2 = new int[size]; // можно
std::cout << sizeof(a1) << " " << size_t(a2) << std::endl; // 0 94376700628656
for (int* i = a2; i != a2 + size; ++i ) {
std::cout << *i;
}
delete[] a2;
return 0;
}
Такой указатель на пустой массив не будет нулевым, но он будет похож на указатель на элемент после конца массива.
Такой указатель можно сравнивать, добавить и вычесть 0, вычесть из себя. Поэтому цикл выше вполне валиден (он кстати не выполнится).
Конечно же, обращаться к значению такого указателя нельзя.
👍3
Как правило, конструктор копирования (copy constructor) принимает как параметр константную ссылку на свой же тип:
В реальности же, первый параметр может быть обычной (не константной) ссылкой на свой тип (хотя смысла в этом обычно нет). Более того, конструктор копирования может принимать и другие аргументы, главное чтоб они имели значение по умолчанию. Пример:
И тут возникает вопрос, а может и ссылка не обязательна? Что если передавать первый аргумент по значению:
Но так делать нельзя, так как для создания копии параметра конструктора копии понадобится вызвать этот же конструктор копии, то есть будет рекурсия. Поэтому первый параметр должен передаваться по ссылке.
Class(const Class & other).В реальности же, первый параметр может быть обычной (не константной) ссылкой на свой тип (хотя смысла в этом обычно нет). Более того, конструктор копирования может принимать и другие аргументы, главное чтоб они имели значение по умолчанию. Пример:
#include <iostream>
class A
{
public:
A() = default;
A(A & other, int i = 10)
{
std::cout << "copy ctor " << i << std::endl;
}
};
void f(A a) // makes copy
{}
int main()
{
A a;
f(a);
return 0;
}
И тут возникает вопрос, а может и ссылка не обязательна? Что если передавать первый аргумент по значению:
Class(Class other) ?Но так делать нельзя, так как для создания копии параметра конструктора копии понадобится вызвать этот же конструктор копии, то есть будет рекурсия. Поэтому первый параметр должен передаваться по ссылке.
👍6🤔1
Тут на собеседовании спросили какие умные указатели бывают, конечно же unique_ptr, shared_ptr и weak_ptr, про это все знают.
Зачем же нужны weak_ptr? Давайте посмотрим на такой вроде бы игрушечный пример:
Из-за циклической зависимости двух объектов деструктор не вызовется!
Конечно же в реальности объекты могут не находиться в коде "рядом" или цикл может быть подлиннее.
Спасением может оказаться weak_ptr, если мы поменяем одну из переменных с shared на weak:
Зачем же нужны weak_ptr? Давайте посмотрим на такой вроде бы игрушечный пример:
#include <iostream>
#include <memory>
class A
{
public:
std::shared_ptr<A> parent;
std::shared_ptr<A> callback;
~A()
{
std::cout << "~A" << std::endl;
}
};
void shared_cycle()
{
std::shared_ptr<A> parent = std::make_shared<A>();
std::shared_ptr<A> child = std::make_shared<A>();
child->parent = parent;
parent->callback = child; // без этой строчки будет ~A ~A
}
int main()
{
shared_cycle();
return 0;
};
Из-за циклической зависимости двух объектов деструктор не вызовется!
Конечно же в реальности объекты могут не находиться в коде "рядом" или цикл может быть подлиннее.
Спасением может оказаться weak_ptr, если мы поменяем одну из переменных с shared на weak:
std::weak_ptr<A> callback;👍5❤2
На реддите пишут, что на собесах иногда спрашивают про CRTP. А вы знали, что это такое?)
CRTP (Curiously Recurring Template Pattern, "любопытный повторяющийся шаблон") - это идиома C++, в которой класс наследуется от реализации шаблонного метода, используя сам себя в качестве шаблонного аргумента.
Пример (из википедии 😁) :
Как видим, Derived наследует Base и передает сам себя в качестве шаблона. Отсюда и рекурсия. Немного напоминает
Зачем это нужно? Так можно достигнуть поведения, аналогичного виртуальным функциям, но без динамического полиморфизма.
Дальнейшее объяснение слишком длинное, предлагаю для подробностей обратиться к хабру.
CRTP (Curiously Recurring Template Pattern, "любопытный повторяющийся шаблон") - это идиома C++, в которой класс наследуется от реализации шаблонного метода, используя сам себя в качестве шаблонного аргумента.
Пример (из википедии 😁) :
template <class T>
struct Base
{
void interface()
{
// ...
static_cast<T*>(this)->implementation();
// ...
}
};
struct Derived : Base<Derived>
{
void implementation();
};
Как видим, Derived наследует Base и передает сам себя в качестве шаблона. Отсюда и рекурсия. Немного напоминает
class Derived : public std::enable_shared_from_this<Derived>.Зачем это нужно? Так можно достигнуть поведения, аналогичного виртуальным функциям, но без динамического полиморфизма.
Дальнейшее объяснение слишком длинное, предлагаю для подробностей обратиться к хабру.
Хабр
CRTP: Пример на паттерне «Мост»
Для кого Эта статья рассчитана на тех, кто не сталкивался с идиомой CRTP (Curiously recurring template pattern), но имеет представление о том, что такое шаблоны в C++. Специфических знаний или...
🔥2❤1👍1
shared_ptr обычно реализуется через подсчет ссылок - количества объектов, которые ссылаются на указатель. При копировании shared_ptr количество увеличивается на 1, деструктор уменьшает количество на 1. При достижении числа 0 вызывается deleter (по-умолчанию delete).
Для иллюстрации воспользуемся методом use_count():
Как видим, weak_ptr создается из shared_ptr, но не увеличивает число shared_ptr, указывающих на этот объект. На самом деле, для weak_ptr имеется отдельный счетчик ссылок, он изменяется при создании "слабой" ссылки.
Для получения указателя из weak_ptr необходимо вызвать метод lock(), который вернет соответствующий shared_ptr, что увеличит число ссылок. Другого способа аналогичного методу get() для получения значения указателя из weak_ptr нет.
weak_ptr может спокойно пережить свой shared_ptr и это безопасно с ним работать после удаления соотв. указателя, но в таком случае lock() вернет указатель на nullptr.
Для иллюстрации воспользуемся методом use_count():
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> shared = std::make_shared<int>(100);
std::cout << shared.use_count() << std::endl; // 1
{
auto shared2 = shared;
std::cout << shared.use_count() << std::endl; // 2
}
std::cout << shared.use_count() << std::endl; // 1
std::weak_ptr<int> weak = shared;
std::cout << shared.use_count() << std::endl; // 1
if (auto shared3 = weak.lock()) { // получить shared_ptr из weak_ptr
std::cout << shared.use_count() << std::endl; // 2
}
std::cout << shared.use_count() << std::endl; // 1
shared.reset(); // чистим
std::cout << shared.use_count() << std::endl; // 0
auto shared4 = weak.lock();
if (shared4 == nullptr) {
std::cout << "can't lock";
}
return 0;
}
// Output:
// 1
// 2
// 1
// 1
// 2
// 1
// 0
// can't lock
Как видим, weak_ptr создается из shared_ptr, но не увеличивает число shared_ptr, указывающих на этот объект. На самом деле, для weak_ptr имеется отдельный счетчик ссылок, он изменяется при создании "слабой" ссылки.
Для получения указателя из weak_ptr необходимо вызвать метод lock(), который вернет соответствующий shared_ptr, что увеличит число ссылок. Другого способа аналогичного методу get() для получения значения указателя из weak_ptr нет.
weak_ptr может спокойно пережить свой shared_ptr и это безопасно с ним работать после удаления соотв. указателя, но в таком случае lock() вернет указатель на nullptr.
❤1👍1🔥1🏆1
Давайте углубимся в детали реализации shared_ptr и может быть немного повторимся.
В типичной реализации, shared_ptr содержит 2 указателя:
- собственно хранимый указатель (тот, что вернет метод get())
- указатель на управляющий блок (control block)
Управляющий блок - это динамически выделяемый объект, который содержит:
- или указатель на управляемый объект (да да, еще один указатель) или сам управляемый объект (подробности дальше)
- deleter
- аллокатор
- число shared_ptr-ов, которые владеют управляемым объектом
- число weak_ptr ("слабый" счетчик), которые владеют управляемым объектом
Когда shared_ptr создается с помощью std::make_shared(), то память под управляющий блок и сам объект создается одним выделением. Управляемый объект создается "на месте" в самом управляющем блоке. Если же создавать shared_ptr через конструктор, то управляемый блок и управляемый объект выделяются отдельно и управляющий блок хранит указатель на объект.
Кстати, вот что пишет Скотт Мэйерс про счетчик ссылок weak_ptr: "На практике значение слабого счетчика не всегда совпадает с количеством указателей std::weak_ptr, ссылающихся на управляющий блок, поскольку разработчики библиотеки нашли способы добавлять в слабый счетчик дополнительную информацию, которая упрощает генерацию лучшего кода". И да, для проверки просроченности указателя, weak_ptr пользуется "обычным" счетчиком ссылок, а не "слабым".
А вот сейчас жесть: указатель, хранимый shared_ptr непосредственно - это тот, который вернется методом get(), тогда как указатель, хранимый управляющим блоком, этот тот, который будет удален когда число шарных указателей достигнет нуля. Эти указатели необязательно равны (хотя с такой ситуацией вы вряд ли столкнетесь)! Если вы ничего не поняли, то это ок , но детали тут.
Деструктор шарного указателя уменьшает число шарных владельцев в контрольном блоке. Если это число падает до нуля, то контрольный блок вызывает деструктор управляемого объекта. Контрольный блок не удаляет сам себя до тех пор. пока и число weak_ptr не упадет до нуля (если weak_ptr не было, то их число сразу 0 и контрольный блок уничтожится).
В типичной реализации, shared_ptr содержит 2 указателя:
- собственно хранимый указатель (тот, что вернет метод get())
- указатель на управляющий блок (control block)
Управляющий блок - это динамически выделяемый объект, который содержит:
- или указатель на управляемый объект (да да, еще один указатель) или сам управляемый объект (подробности дальше)
- deleter
- аллокатор
- число shared_ptr-ов, которые владеют управляемым объектом
- число weak_ptr ("слабый" счетчик), которые владеют управляемым объектом
Когда shared_ptr создается с помощью std::make_shared(), то память под управляющий блок и сам объект создается одним выделением. Управляемый объект создается "на месте" в самом управляющем блоке. Если же создавать shared_ptr через конструктор, то управляемый блок и управляемый объект выделяются отдельно и управляющий блок хранит указатель на объект.
Кстати, вот что пишет Скотт Мэйерс про счетчик ссылок weak_ptr: "На практике значение слабого счетчика не всегда совпадает с количеством указателей std::weak_ptr, ссылающихся на управляющий блок, поскольку разработчики библиотеки нашли способы добавлять в слабый счетчик дополнительную информацию, которая упрощает генерацию лучшего кода". И да, для проверки просроченности указателя, weak_ptr пользуется "обычным" счетчиком ссылок, а не "слабым".
А вот сейчас жесть: указатель, хранимый shared_ptr непосредственно - это тот, который вернется методом get(), тогда как указатель, хранимый управляющим блоком, этот тот, который будет удален когда число шарных указателей достигнет нуля. Эти указатели необязательно равны (хотя с такой ситуацией вы вряд ли столкнетесь)! Если вы ничего не поняли, то это ок , но детали тут.
Деструктор шарного указателя уменьшает число шарных владельцев в контрольном блоке. Если это число падает до нуля, то контрольный блок вызывает деструктор управляемого объекта. Контрольный блок не удаляет сам себя до тех пор. пока и число weak_ptr не упадет до нуля (если weak_ptr не было, то их число сразу 0 и контрольный блок уничтожится).
🔥2❤1👎1
В прошлый раз я писала, что make_shared разместит объект прямо в управляющем блоке, а также что управляющий блок не разрушится, если уничтожены все shared_ptr, но есть хотя бы один weak_ptr, отслеживающий этот контрольный блок. Это значит, что после разрушения последнего шарного указателя память может значительное время не освобождаться (повторюсь, при использовании make_shared).
Давайте проведем небольшой тест: создадим строку размером 100MB с помощью make_shared, затем получим weak_ptr ссылку, сбросим shared_ptr и зациклимся с "живым" weak_ptr:
Что же у меня получилось? Изначально память процесса действительно занимает около 100 МБ, а через несколько секунд (когда shared_ptr сбрасывается) резко падает до < 1 МБ. То есть память все-таки освободилась в момент разрушения последнего shared_ptr.
В чем дело? Да ХЗ, видимо кривой примерчик или компилятор нужно как-то хитрее обмануть. Какие ваши идеи?
Давайте проведем небольшой тест: создадим строку размером 100MB с помощью make_shared, затем получим weak_ptr ссылку, сбросим shared_ptr и зациклимся с "живым" weak_ptr:
#include <iostream>
#include <memory>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
#define MAKE_SHARED 1
constexpr size_t strSize = 100 * 1024 * 1024; // 100 MB
int main()
{
#ifdef MAKE_SHARED
std::shared_ptr<std::string> shared
= std::make_shared<std::string>(strSize, 'z');
#else
std::shared_ptr<std::string> shared;
shared.reset(new std::string(strSize, 'z'));
#endif
std::weak_ptr<std::string> weak(shared);
for (;;) {
std::this_thread::sleep_for(500ms);
if (shared) {
std::cout << (*shared)[0];
}
if (std::rand() % 10 == 0) { // try to fool the compiler
shared.reset();
}
if (std::rand() == -3) { // never happens, but
break; // compiler is stupid
}
}
std::cout << std::endl;
if (auto shared2 = weak.lock()) {
std::cout << (*shared2)[0];
}
return 0;
}
Что же у меня получилось? Изначально память процесса действительно занимает около 100 МБ, а через несколько секунд (когда shared_ptr сбрасывается) резко падает до < 1 МБ. То есть память все-таки освободилась в момент разрушения последнего shared_ptr.
В чем дело? Да ХЗ, видимо кривой примерчик или компилятор нужно как-то хитрее обмануть. Какие ваши идеи?
❤4
Type traits часть 1 (перевод)
Type traits - это фича C++, которая позволяет выполнить compile-time анализ типов в нашем коде.
Использование type traits
Стандартная библиотека C++ включает большую коллекцию type traits и связанных функций в заголовке <type_traits>:
Почти все type traits стандартной библиотеки являются шаблонизированными структурами. Они принимают тип для анализа как параметр шаблона и предоставляют результат анализа через статическое поле value.
Например, std::is_arithmetic сообщает, является ли тип числовым:
Мы можем использовать эти булевы значения как обычно в нашем коде:
Начиная с C++17, есть сокращенный способ получения
Более полезным случаем будет использование трейтов в ситуации, когда мы не знаем типа, то есть с шаблонами:
Type traits - это фича C++, которая позволяет выполнить compile-time анализ типов в нашем коде.
Использование type traits
Стандартная библиотека C++ включает большую коллекцию type traits и связанных функций в заголовке <type_traits>:
#include <type_traits>
Почти все type traits стандартной библиотеки являются шаблонизированными структурами. Они принимают тип для анализа как параметр шаблона и предоставляют результат анализа через статическое поле value.
Например, std::is_arithmetic сообщает, является ли тип числовым:
std::is_arithmetic<int>::value // true
std::is_arithmetic<std::string>::value // false
Мы можем использовать эти булевы значения как обычно в нашем коде:
#include <type_traits>
#include <iostream>
#include <vector>
int main() {
if (std::is_arithmetic<float>::value) {
std::cout << "float is arithmetic";
}
if (!std::is_arithmetic<std::string>::value) {
std::cout << " but std::string isn't";
}
using vector_trait =
std::is_arithmetic<std::vector<int>>;
if (!vector_trait::value) {
std::cout
<< "\nNeither is std::vector<int>";
}
}
// Output:
// float is arithmetic but std::string isn't
// Neither is std::vector<int>
Начиная с C++17, есть сокращенный способ получения
value: добавить _v к имени класса:#include <type_traits>
#include <iostream>
int main() {
if (std::is_arithmetic_v<double>) {
std::cout << "double is arithmetic\n";
}
return 0;
}
Более полезным случаем будет использование трейтов в ситуации, когда мы не знаем типа, то есть с шаблонами:
#include <type_traits>
#include <iostream>
template <typename T>
void Function(T Param) {
if (std::is_arithmetic<T>::value) {
std::cout << "Type is arithmetic\n";
} else {
std::cout << "Type is not arithmetic\n";
}
}
int main() {
Function(100);
Function("Hello World");
return 0;
}
// Output:
// Type is arithmetic
// Type is not arithmetic
❤3👍1🔥1
Type traits часть 2 (перевод)
Compile-type условия с if constexpr
Напоминаем, что type trait вычисляется во время компиляции, а не выполнения. Когда мы хотим выполнить проверку чего-то, известного во время компиляции, то мы можем добавить
Проверка if во время компиляции позволяет полностью удалить весь неподходящий код. Можно представить, что остается такой код:
Рассмотрим еще пример с другими трейтами:
Больше трейтов описано тут.
Compile-type условия с if constexpr
Напоминаем, что type trait вычисляется во время компиляции, а не выполнения. Когда мы хотим выполнить проверку чего-то, известного во время компиляции, то мы можем добавить
constexpr к базовому if (нужен C++17):#include <type_traits>
#include <iostream>
template <typename T>
void Function(T Param) {
if constexpr (std::is_integral_v<T>) {
std::cout << "Integer!\n";
}
if constexpr (std::is_floating_point_v<T>) {
std::cout << "Floating point number!\n";
}
}
int main() {
Function(42);
Function(3.14);
return 0;
}
// Output:
// Integer!
// Floating point number!
Проверка if во время компиляции позволяет полностью удалить весь неподходящий код. Можно представить, что остается такой код:
void Function(int Param) {
std::cout << "Integer!\n";
}
void Function(double Param) {
std::cout << "Floating point number!\n";
}Рассмотрим еще пример с другими трейтами:
#include <type_traits>
#include <iostream>
class Actor {};
class Monster : Actor {};
template <typename T>
void Function(T Param) {
using namespace std;
if constexpr (is_pointer_v<T>) {
cout << "\nType is a pointer";
}
if constexpr (is_class_v<T>) {
cout << "\nType is a class";
}
if constexpr (is_same_v<Actor, T>) {
cout << ": Actor";
} else if constexpr (is_base_of_v<Actor, T>) {
cout << ": Actor (or derived from Actor)";
}
}
int main() {
int x{5};
Function(&x);
Actor MyActor;
Function(MyActor);
Monster MyMonster;
Function(MyMonster);
return 0;
}
// Output:
// Type is a pointer
// Type is a class: Actor
// Type is a class: Actor (or derived from Actor)
Больше трейтов описано тут.
👍4🔥2
Type traits часть 3 (перевод)
Рассмотрим такую простую шаблонную функцию:
Эта функция позволяет передать два числовых значения и получить их среднее. Так как функция шаблонизированная, то мы можем передать любые типы.
Однако такой код позволяет слишком многое. Хотя наша функция предназначения для работы с числами, это явно нигде не обозначено. Что если передвать в нее не числовой тип? Есть два варианта развития событий.
В лучшем случае, используемый тип не будет поддерживать один из операторов, например /. Это приведет к ошибке компиляции, хотя и не очень понятной:
В худжем случае, переданный тип реализует все требуемые операторы, но их поведение не такое же, как предполагает наша функция (а именно арифметическое сложение и деление для + и /). Ведь типы свободны перегружать операторы + и / для любой цели, они не обязаны означать сложение и деление.
В таком случае, компилятор с радостью создаст экземпляр шаблона, так как тип соответвует всем требованиям. И мы не получим ошибку компиляции, хотя получили баг.
Type traits позволяют нам быть более явными относительно требований. Самый базовый пример - это просто использовать static_assert:
Это имеет три преимущества:
- реализация требований. Теперь компилятор будет проверять, что предоставленный тип это число, а не просто что он реализует операторы + и /
- документация требований. Любой, кто взглянет на нашу функцию, увидит какие есть требования. Некоторые IDE смогут сразу предоставить обратную связь до компиляции.
- более полезные сообщения об ошибках, если тип не соответвует требованиям:
Рассмотрим такую простую шаблонную функцию:
template <typename TFirst, typename TSecond>
auto Average(TFirst x, TSecond y) {
return (x + y) / 2;
}
Эта функция позволяет передать два числовых значения и получить их среднее. Так как функция шаблонизированная, то мы можем передать любые типы.
Однако такой код позволяет слишком многое. Хотя наша функция предназначения для работы с числами, это явно нигде не обозначено. Что если передвать в нее не числовой тип? Есть два варианта развития событий.
В лучшем случае, используемый тип не будет поддерживать один из операторов, например /. Это приведет к ошибке компиляции, хотя и не очень понятной:
binary '/':
'std::basic_string<char,std::char_traits<char>,std::allocator<char>>'
does not define this operator or a conversion to a type acceptable
to the predefined operator
В худжем случае, переданный тип реализует все требуемые операторы, но их поведение не такое же, как предполагает наша функция (а именно арифметическое сложение и деление для + и /). Ведь типы свободны перегружать операторы + и / для любой цели, они не обязаны означать сложение и деление.
В таком случае, компилятор с радостью создаст экземпляр шаблона, так как тип соответвует всем требованиям. И мы не получим ошибку компиляции, хотя получили баг.
Type traits позволяют нам быть более явными относительно требований. Самый базовый пример - это просто использовать static_assert:
#include <type_traits>
template <typename TFirst, typename TSecond>
auto Average(TFirst x, TSecond y) {
static_assert(
std::is_arithmetic<TFirst>::value,
"Average: First argument must be arithmetic");
static_assert(
std::is_arithmetic<TSecond>::value,
"Average: Second argument must be arithmetic");
return (x + y) / 2;
}
Это имеет три преимущества:
- реализация требований. Теперь компилятор будет проверять, что предоставленный тип это число, а не просто что он реализует операторы + и /
- документация требований. Любой, кто взглянет на нашу функцию, увидит какие есть требования. Некоторые IDE смогут сразу предоставить обратную связь до компиляции.
- более полезные сообщения об ошибках, если тип не соответвует требованиям:
int main() {
Average("Hello", "World");
}static_assert failed: 'Average: First argument must be arithmetic'
static_assert failed: 'Average: Second argument must be arithmetic'
👍5
Задачка для собеса. Пусть дан вектор беззнаковых целых, в которых есть нули. Нужно сдвинуть все нули в конец вектора, то есть написать код функции, используя стандартные алгоритмы:
Конечно, можно написать код самому, но так вы не покажете знания std. Как вариант, можно сделать сортировку в обратном порядке:
но тогда порядок нарушится и плюс это занимает O(N·log(N)).
Другим более интересным вариантом будет использование std::remove:
Дело в том что remove (в отличие от std::erase) не удаляет физически элементы из контейнера (про это уже было писано на канале). Вместо этого, нужные элементы сдвигаются в начало контейнера, а в конце остается "мусор". Нам остается только забить нулями "мусор", начало которого remove и вернет.
#include <iostream>
#include <vector>
#include <algorithm>
void move_zeroes_to_end(std::vector<size_t>& v)
{
// TODO
}
int main()
{
std::vector<size_t> v = { 0, 4, 8, 3, 7, 0, 1, 4, 4, 0 };
move_zeroes_to_end(v);
for (auto i : v) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
Конечно, можно написать код самому, но так вы не покажете знания std. Как вариант, можно сделать сортировку в обратном порядке:
std::sort(v.rbegin(), v.rend());но тогда порядок нарушится и плюс это занимает O(N·log(N)).
Другим более интересным вариантом будет использование std::remove:
auto zero_it = std::remove(v.begin(), v.end(), 0);
std::fill(zero_it, v.end(), 0);
Дело в том что remove (в отличие от std::erase) не удаляет физически элементы из контейнера (про это уже было писано на канале). Вместо этого, нужные элементы сдвигаются в начало контейнера, а в конце остается "мусор". Нам остается только забить нулями "мусор", начало которого remove и вернет.
👍3❤1
Оператор присвоения копии получает аргумент того же типа, что и класс.
Для совместимости с оператором присвоения встроенных типов операторы присвоения обычно возвращают (неконстантную) ссылку на свой левый операнд:
Как видим, благодаря возврату ссылки, работает множественное присваивание:
Однако, оператор присвоения копии может возвращать любое значение.
Если мы захотим возвращать ничего, то это тоже возможно:
В этом случае, множественное присваивание не сработает, однако единичное вполне:
Мы также можем (например по ошибке) вернуть не ссылку, а сам объект, а также например строку:
Конечно, так делать не рекомендуется.
Кстати, компилятор будет синтезировать оператор присвоения копии с возвратом неконстантной (кто знает почему?) ссылки на левый операнд.
Для совместимости с оператором присвоения встроенных типов операторы присвоения обычно возвращают (неконстантную) ссылку на свой левый операнд:
#include <iostream>
class Data
{
public:
Data(int i = 0) : _i(i) {}
Data& operator=(const Data & other) // copy assignment
{
std::cout << "operator= " << _i << std::endl;
_i = other._i;
return *this;
}
int i() const { return _i; }
private:
int _i;
};
int main() {
Data d1(1);
Data d2(2);
Data d3(3);
d1 = d2 = d3;
std::cout << d1.i() << d2.i() << d3.i() << std::endl;
return 0;
}
// Output:
// operator= 2
// operator= 1
// 333
Как видим, благодаря возврату ссылки, работает множественное присваивание:
d1 = d2 = d3;.Однако, оператор присвоения копии может возвращать любое значение.
Если мы захотим возвращать ничего, то это тоже возможно:
void operator=(const Data & other)В этом случае, множественное присваивание не сработает, однако единичное вполне:
d1 = d2 = d3; // ошибка!
d1 = d2; // ок
Мы также можем (например по ошибке) вернуть не ссылку, а сам объект, а также например строку:
#include <iostream>
#include <string>
class Data
{
public:
Data(int i = 0) : _i(i) {}
std::string operator=(const Data & other) // copy assignment
{
std::cout << "operator= " << _i << std::endl;
_i = other._i;
return "lovite narkomana";
}
int i() const { return _i; }
private:
int _i;
};
int main() {
Data d1(1);
Data d2(2);
std::cout << (d1 = d2) << std::endl;
return 0;
}
// Output:
// operator= 1
// lovite narkomana
Конечно, так делать не рекомендуется.
Кстати, компилятор будет синтезировать оператор присвоения копии с возвратом неконстантной (кто знает почему?) ссылки на левый операнд.
🤔1
Type traits часть 4 (перевод)
std::enable_if и SFINAE
Трейт std::enable_if принимает два параметра - булево значение и тип. Если значение булева параметра является истинным, то результирующая структура (enable_if это параметризированная структура) содержит поле
Представим такой код:
Во время компиляции, можно представить что наш enable_if код выльется просто в
Обычно мы используем алиас std::enable_if_t, который дает прямой доступ к
Давайте посмотрим, что будет если булево значение ложно:
В этом случае можно представить, что код эквивалентен такому:
Этот код невалиден и мы получим ошибку компиляции. И какая от всего этого польза? Она связана с правилом из мира шаблонов, называемого SFINAE (substitution failure is not an error - «неудавшаяся подстановка — не ошибка»). В общем, правило гласит: Если не получается рассчитать окончательные типы шаблонных параметров функции, компилятор не выбрасывает ошибку. Этот шаблон просто игнорируется и компилятор переходит к другим кандидатам.
Мы можем создать шаблонные функции, которые в зависимости от условий могут привести к ошибке подстановки. Например:
Если T это класс, то наша подставка будет выглядеть как-то так:
Код выше вполне себе валидный, так что наша функция будет способна обработать наш вызов. Однако, если T это не класс, то шаблон после подстановки будет выглядеть вот так:
В этот раз получившийся код не является валидным. Но, согласно правилу SFINAE, ошибка подстановки не обязательно значит ошибку компиляции. Ведь может быть другой шаблон, который подойдет.
В примере ниже, у нас есть шаблон, который приведет к ошибке подстановки, если тип является классом и другой, если тип не является классом.
Эта программа успешно скомпилится и выполнится, потому что все вызовы функций были покрыты шаблонами, которые не пострадали от ошибки подстановки.
Если код выше выглядит непонятным - то это так. Минимум - это запомнить, что такое правило есть и понять в чем его суть.
Стоит отметить, что в C++20 для упрощения темы шаблонного программирования были добавлены концепты.
std::enable_if и SFINAE
Трейт std::enable_if принимает два параметра - булево значение и тип. Если значение булева параметра является истинным, то результирующая структура (enable_if это параметризированная структура) содержит поле
type с типом второго параметра.Представим такой код:
#include <type_traits>
int main() {
std::enable_if<true, int>::type MyInt{42};
}
Во время компиляции, можно представить что наш enable_if код выльется просто в
int, что сделает наш код эквивалентным:#include <type_traits>
int main() {
int MyInt{42};
}
Обычно мы используем алиас std::enable_if_t, который дает прямой доступ к
type:std::enable_if_t<true, int> MyInt{42};Давайте посмотрим, что будет если булево значение ложно:
#include <type_traits>
int main() {
std::enable_if<false, int>::type MyInt{42};
}
В этом случае можно представить, что код эквивалентен такому:
#include <type_traits>
int main() {
MyInt{42};
}
Этот код невалиден и мы получим ошибку компиляции. И какая от всего этого польза? Она связана с правилом из мира шаблонов, называемого SFINAE (substitution failure is not an error - «неудавшаяся подстановка — не ошибка»). В общем, правило гласит: Если не получается рассчитать окончательные типы шаблонных параметров функции, компилятор не выбрасывает ошибку. Этот шаблон просто игнорируется и компилятор переходит к другим кандидатам.
Мы можем создать шаблонные функции, которые в зависимости от условий могут привести к ошибке подстановки. Например:
template <typename T,
std::enable_if_t<std::is_class_v<T>, int> = 0>
void Function(T Arg) {}
Если T это класс, то наша подставка будет выглядеть как-то так:
template <typename T, int = 0>
void Function(T Arg) {}
Код выше вполне себе валидный, так что наша функция будет способна обработать наш вызов. Однако, если T это не класс, то шаблон после подстановки будет выглядеть вот так:
template <typename T, = 0>
void Function(T Arg) {}
В этот раз получившийся код не является валидным. Но, согласно правилу SFINAE, ошибка подстановки не обязательно значит ошибку компиляции. Ведь может быть другой шаблон, который подойдет.
В примере ниже, у нас есть шаблон, который приведет к ошибке подстановки, если тип является классом и другой, если тип не является классом.
#include <type_traits>
#include <iostream>
template <typename T,
std::enable_if_t<std::is_class_v<T>, int> = 0>
void Function(T Arg) {
std::cout << "That was a class\n";
}
template <typename T,
std::enable_if_t<!std::is_class_v<T>, int> = 0>
void Function(T Arg) {
std::cout << "That wasn't\n";
}
class Character {};
int main() {
Function(Character{});
Function(3.14);
}
// Output:
// That was a class
// That wasn't
Эта программа успешно скомпилится и выполнится, потому что все вызовы функций были покрыты шаблонами, которые не пострадали от ошибки подстановки.
Если код выше выглядит непонятным - то это так. Минимум - это запомнить, что такое правило есть и понять в чем его суть.
Стоит отметить, что в C++20 для упрощения темы шаблонного программирования были добавлены концепты.
👍1
Исходники генератора подземелий для вашей визитки 😁.
https://gist.github.com/munificent/b1bcd969063da3e6c298be070a22b604
#юмор
https://gist.github.com/munificent/b1bcd969063da3e6c298be070a22b604
#юмор
❤🔥1😁1
Type traits часть 5 (перевод)
Условный выбор типа с помощью std::conditional
В заголовке
Несколько примеров:
Обычно,
Условный выбор типа с помощью std::conditional
В заголовке
<type_traits> есть шаблон std::conditional, который позволяет нам динамически выбирать тип на этапе компиляции. Он принимает три шаблонных параметра - булево значение, тип для использования, если первый параметр true, и тип, если он false. Результирующий тип доступен в статическом члене type.Несколько примеров:
using namespace std;
// здесь будет int
conditional<true, int, float>::type
// Можем использовать conditional_t для доступа к ::type напрямую
// здесь будет std::string
conditional_t<true, string, char*>
// Булев параметр может быть любым выражением, известным на момент компиляции
// здесь будет std::string
constexpr bool useStdString{true};
conditional_t<useStdString, string, char*>
// здесь будет std::vector<float>
conditional_t<false, vector<int>, vector<float>>
// здесь также будет std::vector<float>
vector<conditional_t<false, int, float>>
Обычно,
std::conditional используется для создания псевдонима/алиаса, который мы затем можем легко использовать в нашем коде:#include <iostream>
#include <type_traits> // for std::conditional
#include <typeinfo> // for typeid
constexpr bool Use64BitIntegers{true};
using integer =
std::conditional<Use64BitIntegers,
std::int64_t,
std::int32_t>::type;
int main() {
integer MyInt{42};
std::cout << typeid(integer).name() << " ("
<< sizeof(integer) << " bytes)";
}
// Output (варианты, зависит от компилятора):
// __int64 (8 bytes)
// l (8 bytes)
🔥2
Начиная с C++17, в лямбде можно захватить this по значению через *this (просто this захватит объект по ссылке):
Копия объекта может понадобиться например для исполнения лямбды в другом потоке, когда this уже может быть инвалидирован на момент вызова лямбды.
https://habr.com/ru/companies/otus/articles/794821/
class MyClass {
public:
int value = 10;
void printValue() {
auto lambda = [=, *this]() {
std::cout << "Value: " << value << std::endl;
};
lambda();
}
};
Копия объекта может понадобиться например для исполнения лямбды в другом потоке, когда this уже может быть инвалидирован на момент вызова лямбды.
https://habr.com/ru/companies/otus/articles/794821/
Хабр
Краткий обзор развития лямбда-выражений в C++11, C++14, C++17 и C++20
Привет, Хабр! Сегодня рассмотрим лямбда-выражения в C++ и их эволюцию с момента появления в стандарте C++11 и до последних обновлений в C++20. Лямбда - выражения в C++ — это анонимные функции, которые...
👍4