Маша С++
104 subscribers
54 photos
2 files
65 links
Учу C++ и не только вместе с вами, мои котятки)
Download Telegram
Рассмотрим следующий код, в котором определен конструктор копирования для производного класса:
#include <iostream>

class Base
{
public:
Base() : bValue(3) {}
int bValue;
};

class Derived : public Base
{
public:
Derived() = default;
Derived(const Derived& other) : dValue(other.dValue) {}
int dValue = 0;
};

int main() {
Derived d1;
d1.dValue = 4;
d1.bValue = 7;
Derived d2(d1); // делаем копию
std::cout << d2.dValue << " " << d2.bValue << std::endl; // 4 3
}

Как видим, мы ожидали вывод 4 7, а получили 4 3, то есть конструктор копирования не скопировал значение из базового класса, а вызвал его конструктор по-умолчанию. Это ожидаемое поведение, ведь мы "забыли" *явно* вызвать базовый конструктор копирования. Исправим ситуацию:
Derived(const Derived& other) : Base(other), dValue(other.dValue) {}

Так будет 4 7

Важно понимать, что конструктор копирования базового класса скопирует только члены базового класса, иначе говоря, если написать Derived(const Derived& other) : Base(other) {}, то вывод будет неверным: 0 7

То же самое верно и для конструктора перемещения наследника:
Derived (Derived && d) : Base (std::move(d)) {/* init Derived vars */}
👍4👀3
Если не указать атрибут наследования, то для производного класса объявленного как class будет приватное наследование, а для struct - публичное:
#include <iostream>
#include <map>

class Base
{
public:
void f() {}
};

class Derived1 : Base // не указан атрибут наследования
{

};

struct Derived2 : Base // не указан атрибут наследования
{

};

int main()
{
Derived1 d1;
Derived2 d2;

d1.f(); // Ошибка, private inheritance
d2.f(); // OK, public inheritance

return 0;
}
🫡6
Начиная с С++11 параметр типа шаблона можно сделать дружественным, пример:
#include <iostream>

template <typename T>
class Alice
{
friend T; // не friend class T
int priv_int = 4;
};

class Bob
{
public:
void print(const Alice<Bob>& alice) {
std::cout << alice.priv_int // Bob is friend of Alice
<< std::endl;
}
};

int main()
{
Alice<Bob> a;
Bob b;

b.print(a); // 4

return 0;
}

Класс Bob стал другом класса Alice<Bob> и получил доступ до его приватных членов.
🔥8
Можно ли вызывать виртуальные функции из конструкторов и деструкторов? Можно, но поведение может быть неожиданным (и такого лучше избегать). Пример:
#include <iostream>

class Base
{
public:
Base() {
std::cout << "Base::Base" << std::endl;
f1(); // всегда вызов Base::f1, даже если он переопределен в наследнике
}
virtual ~Base() {
std::cout << "Base::~Base" << std::endl;
f2(); // всегда вызов Base::f2, даже если он переопределен в наследнике
};
virtual void f1() { std::cout << "Base::f1" << std::endl; }
virtual void f2() { std::cout << "Base::f2" << std::endl; }
};

class Derived : public Base
{
public:
Derived() {
std::cout << "Derived::Derived" << std::endl;
f1();
}
~Derived() override {
std::cout << "Derived::~Derived" << std::endl;
f2();
};
void f1() override { std::cout << "Derived::f1" << std::endl; }
void f2() override { std::cout << "Derived::f2" << std::endl; }
};

int main() {
Derived d;
std::cout << "============" << std::endl;
d.f2();
std::cout << "************" << std::endl;
}

// Output:
// Base::Base
// Base::f1
// Derived::Derived
// Derived::f1
// ============
// Derived::f2
// ************
// Derived::~Derived
// Derived::f2
// Base::~Base
// Base::f2


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

При вызове деструктора будет похожая ситуация, но в обратном порядке. При вызове деструктора базового класса производная часть уже будет удалена то есть уже будет вызыван деструктор производного класса. При этом виртуальный метод в базовом деструкторе будет вызван для базового типа.
👍4
У шаблона класса могут быть статические члены, при этом у каждого экземпляра для любого конкретного типа будет свой экземпляр:
#include <iostream>

template <typename T>
class Class
{
public:
static int size() {
return sz;
}
static int sz;
T value;
};

template <typename T>
int Class<T>::sz = 0; // дефолтное значение для всех экземпляров шаблонов


int main() {
Class<float> fc1, fc2, fc3;
Class<int> ic;
fc1.sz = 3;
ic.sz = 4;
std::cout << fc1.size() << " " << fc2.size()
<< " " << Class<float>::size(); // 3 3 3
std::cout << std::endl;
std::cout << ic.size(); // 4
}

Как видим у экземляра Class<float> свои экземпляры для статических членов size() и sz, а у Class<int> свои, отличные от них.
❤‍🔥6
Давайте набросаем простой класс, который поможет отследить вызов конструктора и деструктора:
struct Xray
{
Xray(std::string value)
: mValue(std::move(value))
{
std::cout << "Xray ctor, value is " << mValue << std::endl;
}

Xray(const Xray& other)
: mValue(other.mValue)
{
std::cout << "Xray const& ctor, value is " << mValue << std::endl;
}

~Xray()
{
std::cout << "~Xray dtor, value is " << mValue << std::endl;
}

std::string mValue;
};


Далее мы просто создадим временное значение Xray:
void main()
{
// Вывод: Xray ctor, value is 1
// Вывод: Xray dtor, value is 1
Xray{"1"};

std::cout << "Wait a sec" << std::endl;
}


Как видим, деструктор вызовется в той же строке кода, что и конструктор, то есть до вывода "Wait a sec", то есть до закрываюшей скобки.

Теперь же создадим ссылку (констанную lvalue) на временный объект:
void main()
{
// Вывод: Xray ctor, value is 1
const Xray& xrayRef = Xray{"1"};

// Вывод: xrayRef value is 1
std::cout << "xrayRef value is " << xrayRef.mValue << std::endl;

} // Вывод: Xray dtor, value is 1

Получили другой и возможно ожидаемый для первого случая вывод.

В чем же дело? Виноват механизм продления времени жизни временных объектов, который звучит так:
Если вы приняли временный объект по константной lvalue ссылке или rvalue ссылкe, то его время жизни будет продлено до времени жизни ссылки.

Прочитать подробнее можно в источнике.
👍3
Прохожу курс, там сказано сравните время вставки в конец вектора и дека миллиона и 5 млн случайных чисел. Напишем код:
#include <vector>
#include <deque>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <chrono>
#include <string>

void fill_random_vector(size_t size)
{
std::vector<int> v;
for (size_t i = 0; i < size; ++i) {
v.push_back(rand());
}
std::cout << "v[" << size - 1 << "] = " << v[size - 1] << std::endl;
}

void fill_random_deque(size_t size)
{
std::deque<int> d;
for (size_t i = 0; i < size; ++i) {
d.push_back(rand());
}
std::cout << "d[" << size - 1 << "] = " << d[size - 1] << std::endl;
}

template<typename F>
void measure_time(F func, const std::string& text)
{
auto begin = std::chrono::steady_clock::now();
func();
auto end = std::chrono::steady_clock::now();

std::cout << text << " "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count()
<< " ms" << std::endl;
}

int main()
{
srand((unsigned)time(0));

measure_time([](){ fill_random_vector(1'000'000); }, "fill_random_vector 1'000'000");
measure_time([](){ fill_random_deque(1'000'000); }, "fill_random_deque 1'000'000");

measure_time([](){ fill_random_vector(5'000'000); }, "fill_random_vector 5'000'000");
measure_time([](){ fill_random_deque(5'000'000); }, "fill_random_deque 5'000'000");

return 0;
}

После запуска на 5 разных платформах получилось, что дек быстрее на 3, на одной примерно также, а на последней (MSVC) медленнее. По мнению авторов курса, дек быстрее, тк там нет копирования при ресайзе (reserve для вектора сказано не использовать).

Лично у меня два вывода: 1) берем вектор и не паримся, он все равно очень быстрый 2) такие вопросы без окружения в тестирование выносить - дурной тон.
👍4
Шпаргалка по "чудесному" миру строк в WinAPI (источник)

А еще есть OLECHAR, BSTR и куча макросов)
2👍1
Удаляет ли std::vector элементы (иначе, вызывает ли деструктор) при resize() и clear()? И если да, то в каком порядке?
Напишем код:
#include <iostream>
#include <vector>

struct DtorLog
{
DtorLog(int v = 0) : value(v) {}
~DtorLog() { std::cout << "~" << value << std::endl; }
int value;
};

int main()
{
{
std::vector<DtorLog> v;
v.reserve(10);
for (int i = 1; i <= 5; ++i) {
v.emplace_back(i);
}

std::cout << "resize " << v.capacity() << std::endl;
v.resize(3);
std::cout << "clear " << v.capacity() << std::endl;
v.clear();
std::cout << "shrink_to_fit " << v.capacity() << std::endl;
v.emplace_back(6);
v.emplace_back(7);
v.shrink_to_fit();
std::cout << "end " << v.capacity() << std::endl;
}

std::cout << "array" << std::endl;

DtorLog a[3];
for (int i = 0; i < 3; ++i) {
a[i].value = i;
}

return 0;
}

Проанализируем вывод:
resize 10
~4
~5
clear 10
~1
~2
~3
shrink_to_fit 10
~6
~7
end 2
~6
~7
array
~2
~1
~0

Несмотря на то, что capacity не меняется, для элементов вызывается деструктор. При этом деструктор неожиданно вызывается в порядке от первого элемента к последнему, что противоречит обычному правилу: деструктор вызывается в порядке обратном конструктору. Хотя следует упомянуть, что порядок удаления для вектора не определен (FIXME) и может быть другим/любым. Однако же для обычного массива он будет ожидаемым - сперва удалится последний элемент.
👍6
std::atexit регистрирует функцию для вызова при "нормальном" завершении программы (например выходе из main или вызова std::exit).

Если программа завершается аварийно, например из-за terminate(), то вызов не срабатывает. Давайте напишем программу, которая выведет в лог текст, если не "крэшнулась". Крэш сэмулируем с вероятностью 50% с помощью исключения:

#include <cstdlib>
#include <iostream>
#include <time.h>

void atexit_handler()
{
std::cout << "Hooray, not crashed" << std::endl;
}

int main()
{
std::atexit(atexit_handler);

srand(time(NULL));

if (rand() % 2 == 0) {
throw std::runtime_error("Let's crash");
}

std::cout << "app end" << std::endl;

return 0;
}


В случае нормального завершения вывод будет:
app end
Hooray, not crashed

а при исключении - ни одна из строк.
При этом обработчиков-функций может быть несколько (как минимум 32).
👍7
В продолжении темы про аварийное завершение работы. Что делать, если статическая или thread_local переменная в конструкторе выбрасывает исключение? Ничего другого, кроме как вызвать std::terminate нет.

#include <iostream>

class ThrowsClass
{
public:
ThrowsClass()
{
throw std::runtime_error("oh shit");
}

~ThrowsClass()
{
throw std::runtime_error("dtor");
}

void print()
{
std::cout << "throws";
}
};

static ThrowsClass t; // std::terminate
thread_local ThrowsClass tl; // std::terminate

int main() {

tl.print();
std::cout << "end";
}

Следует отметить, что это верно и для исключения в деструкторе.
👍4
Рассмотрим шаблон функции, который задает не только типы аргументов, но и тип результата, например дробное или целочисленное деление:
template <typename T1, typename T2, typename T3>
T1 div(T2 a, T3 b) {
return (T1)a / b;
}


Мы могли бы вызвать этот код так:
int main()
{
auto res = div<double>(3, 2);
std::cout << res << std::endl; // 1.5
return 0;
}

Код скомпилируется и верно отработает, выдав заказанное нами дробное значение. Типы int для аргументов он выведет автоматом и нам их не нужно писать в угловых скобках (double написать все же придется).

Ну и что такого, спросите вы, это же ожидаемо? А давайте посмотрим, что было бы, если бы мы написали эту же функцию, но с другим порядком типов:

template <typename T1, typename T2, typename T3>
T3 div_v2(T1 a, T2 b) {
return (T3)a / b;
}
auto res2 = div_v2<int>(3, 2); // won't compile!

Этот код не скомпилируется и нам пришлось бы писать все 3 параметра, чтоб уважить компилятор:
auto res2 = div_v2<int, int, int>(3, 2); // OK

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

#cpp #templates
👍4
Всем известно, что язык C++ является прямым потомком языка C и позаимствовал из него очень многое. Но работает ли это в обратную сторону? Судите сами. Изменения в стандарте C23 (не С++23!) среди прочего включают:
- изменения в auto для: auto y = cos(x);
- поддержка синтаксиса [[attr]], например: [[deprecated]]
- добавлены ключевые слова bool, static_assert и другие
- добавлены nullptr_t и nullptr
- добавлен разделитель ': 1'000'000
- поддержка constexpr

Подробнее тут: https://www.opennet.ru/opennews/art.shtml?num=62278
👍6
Как быстро и просто инвертировать вектор? Передадим reverse-итераторы в конструктор вектора, получим нужную копию:
std::vector<int> reverse(const std::vector<int>& source) {
return std::vector(source.crbegin(), source.crend());
}


Коротко, но может быть не самый эффективный вариант? Что ж, сравним с такой мамкиной реализацией:
std::vector<int> reverse2(const std::vector<int>& source) {
std::vector<int> res;
res.reserve(source.size());

for (auto it = source.crbegin(); it != source.crend(); ++it) {
res.push_back(*it);
}

return res;
}


После нескольких замеров оказалось, что более короткая реализация при N = 10М выполняется за 12 мс, а более длинная за 19мс. Уотак вот, можно пользоваться и радоваться мощи итераторов.
👍3🔥2
Проходили тут курс по python, и там конечно же сказано, что в python отступы в блоках обязательны, а во многих других языках - нет. И мне даже показалось, что это в недостатки других языков внесли. Типа вот глядите какую нечитаемую фигню можно на C написать, а на python всё замечательно. И приводят такой пример (кстати, рекомендую запустить код):

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include <unistd.h>
k;double sin()
,cos();main(){float A=
0,B=0,i,j,z[1760];char b[
1760];printf("\x1b[2J");for(;;
){memset(b,32,1760);memset(z,0,7040)
;for(j=0;6.28>j;j+=0.07)for(i=0;6.28
>i;i+=0.02){float c=sin(i),d=cos(j),e=
sin(A),f=sin(j),g=cos(A),h=d+2,D=1/(c*
h*e+f*g+5),l=cos (i),m=cos(B),n=
sin(B),t=c*h*g-f* e;int x=40+30*D*
(l*h*m-t*n),y= 12+15*D*(l*h*n
+t*m),o=x+80*y, N=8*((f*e-c*d*g
)*m-c*d*e-f*g-l *d*n);if(22>y&&
y>0&&x>0&&80>x&&D>z[o]){z[o]=D;;;b[o]=
".,-~:;=!*#$@"[N>0?N:0];}}/*#****!!-*/
printf("\x1b[H");for(k=0;1761>k;k++)
putchar(k%80?b[k]:10);A+=0.04;B+=
0.02;}}/*****####*******!!=;:~
~::==!!!**********!!!==::-
.,~~;;;========;;;:~-.
..,--------,*/

P.S. на телефоне разметка может разъехаться.
😁91
Наверное, все знают про перечисления с ограничением области видимости enum class:

enum class Color {
Red,
Green,
Blue
};

А кто из вас знал, что можно использовать enum struct ? (👍🏻👎🏻)
👍72
Посмотрите на этот код:
%:include <iostream>

struct X
<%
compl X() <%%> // destructor
X() <%%>
X(const X bitand) = delete; // copy constructor
// X(X and) = delete; // move constructor

bool operator not_eq(const X bitand other)
<%
return this not_eq bitand other;
%>
%>;

int main(int argc, char* argv<::>)
<%
// lambda with reference-capture:
auto greet = <:bitand:>(const char* name)
<%
std::cout << "Hello " << name << " from " << argv<:0:> << '\n';
%>;

if (argc > 1 and argv<:1:> not_eq nullptr)
greet(argv<:1:>);
else
greet("Anon");
%>


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

Что это такое? Альтернативные токены.
👍4
Читаю тут статью ~20-летней давности. Улыбнуло, что БД размером в 9Гб в банке (!) на тот момент оказалась проблемой. Всё-таки всё очень сильно изменилось с тех пор (привет, электрон), а Итаниум давно почил в бозе.

В качестве примера можно привести интеграцию Альфа-Банком в свою IT-инфраструктуру платформы на базе Itanium 2. Рост инвестиционного бизнеса банка привел к тому, что система в имеющейся конфигурации перестала справляться с возрастающей нагрузкой: задержки в обслуживании пользователей временами достигали критической отметки. Анализ ситуации показал, что узким местом системы является не производительность процессоров, а ограничение 32-разрядной архитектуры в части подсистемы памяти, не позволяющей эффективно использовать больше 4 Гбайт адресного пространства сервера. Объем базы данных превышал 9 Гбайт. Использовалась она весьма интенсивно, что приводило к критической загрузке подсистемы ввода-вывода. Альфа-Банк принял решение о закупке кластера из двух четырехпроцессорных серверов на базе Itanium 2 с объемом оперативной памяти — 12 Гбайт.
3👍2
#база
Какой из обработчиков catch сработает в таком коде?
#include <iostream>

class BaseException : public std::exception {};
class DerivedException : public BaseException {};

int main(int argc, char* argv<::>)
{
try
{
throw DerivedException();
}
catch (const std::exception&)
{
std::cout << "std::exception" << std::endl;
}
catch (const BaseException&)
{
std::cout << "BaseException" << std::endl;
}
catch (const DerivedException&)
{
std::cout << "DerivedException" << std::endl;
}

return 0;
}

Не смотря на то, что блок с DerivedException выглядит наиболее подходящим, выберется первый блок catch, который сможет обработать исключение, то есть std::exception, потому что он является родителем DerivedException.
Это значит, что наиболее специализированные исключения нужно располагать первыми, то есть порядок в коде должен быть обратным.
👍41
Нам всегда говорили, что бросать исключения из деструктора это плохая идея. Один из примеров плохого результата - это раскручивание стека при обработке исключения, когда вызываемые деструкторы приводят к повторному исключению, которое кроме как вызовом terminate() толком не обработаешь. Но что, если выбросить исключение очень хочется, ведь мы сделаем всё "аккуратно"? Давайте посмотрим на такой код:
#include <iostream>

struct ThrowsInDtor
{
~ThrowsInDtor()
{
std::cout << "~ThrowsInDtor" << std::endl;
throw std::runtime_error("holy...");
}
};

int main(int argc, char* argv<::>)
{
try
{
ThrowsInDtor dtor;
}
catch (const std::runtime_error& ex)
{
std::cout << "ex: " << ex.what() << std::endl;
}

return 0;
}


Что тут может пойти не так, вроде же всё ОК? Одно исключение, мы его ловим. По факту, данный код приведет к вызову terminate(), так как все деструкторы по-умолчанию неявно помечены как noexcept, то есть не выбрасывающие исключений.

Если очень сильно хочется вернуться во времена до C++11, то помечаем деструктор как noexcept(false) и получаем наше исключение (не понятно только зачем):
~ThrowsInDtor() noexcept(false)
👍5