Маша С++
104 subscribers
54 photos
2 files
65 links
Учу C++ и не только вместе с вами, мои котятки)
Download Telegram
Член производного класса с тем же именем, что и в базовом, скрывает член базового класса (скрывает к нему доступ).
#include <iostream>

struct Base
{
Base(int i) : _protMember(i) {}
int get() const { return _protMember; }
protected:
int _protMember;
};

struct Derived : public Base
{
Derived(int i) : _protMember(i), Base(0) {}
int get() const { return _protMember; }
int getBase() const { return Base::_protMember; }
protected:
int _protMember;
};

struct Derived2 : public Base
{
Derived2(int i) : Base(i) {}
};

int main()
{
Derived d(42);
std::cout << d.get() << " " << d.getBase() << std::endl; // 42 0
std::cout << sizeof(Derived) << " " << sizeof(Derived2) << std::endl; // 8 4

return 0;
}

При этом важно, что "скрытие" не означает замену. Член базового класса продолжает существовать (см sizeof), а для доступа к члену базового класса можно использовать оператор области видимости (Base::).
👍2
В С++17 в std::map добавили метод try_emplace, который похож на emplace, но он создает элемент, только если такого ключа еще нет (есть еще другие особенности). Обычный emplace создает элемент, даже если вставка не произойдет. Таким образом, try_emplace позволяет ускорить код, если создание объекта тяжелое. Пример:
#include <iostream>
#include <map>

struct Value
{
Value(int i = 0)
{
std::cout << "Value " << i << std::endl;
}

Value(const Value& other)
{
std::cout << "Value copy ctor" << std::endl;
}

Value& operator=(const Value& other)
{
std::cout << "Value operator=" << std::endl;
return *this;
}
};

struct Key
{
Key(int i = 0) : _i(i)
{
std::cout << "Key " << i << std::endl;
}

bool operator<(const Key& other) const
{
return _i < other._i;
}
int _i;
};

int main()
{
// Key 1
// Value 1
// Value copy ctor
std::map<Key, Value> map = {
{1, 1}
};
std::cout << std::endl;

// Value 2
// Key 2
// Value 0 - вызвался дефолтный конструктор, он требуется для operator[]
// Value operator=
map[2] = 2;
std::cout << std::endl;

// Key 3
// Value 3
// Value copy ctor
map.insert({3, 3});
std::cout << std::endl;

// Key 4
// Value 4 - экономим один вызов конструктора!
map.emplace(4, 4);
std::cout << std::endl;

// А теперь добавим элемент, который уже есть
// Key 3
// Value 3 - элемент уже есть, но мы его все равно создадим
map.emplace(3, 3);

std::cout << std::endl;

// Key 3 - элемент создастся, только если такого еще нет
const auto& [it, inserted] = map.try_emplace(3, 3);
// Not inserted
if (!inserted) std::cout << "Not inserted" << std::endl;

return 0;
}
👍7
Как определить, начинается ли строка с подстроки?
Если у вас есть C++20, то используем starts_with и радуемся краткому и понятному коду.
Если же его нет, то можно использовать compare, который возвращает 0 если (под) строки равны:
std::string prefix("Who");
if (str.compare(0, prefix.size(), prefix) == 0)

Менее читаемое, чем первый вариант, но как-будто бы терпимо.

Если же вы хотите напрячь мозги своего коллеги-джуна, то используйте std::string::rfind, передав 0 в качестве позиции:
std::string str = "Who let the dogs out";
if (s.rfind("Who", 0) == 0) {


Давайте прочитаем доку: rfind ищет *последнюю* подстроку, которая совпадает с переданной последовательностью символов. Когда передан параметр pos (второй), то поиск включает символы которые начинаются в или до позиции pos, игнорируя все возможные совпадения начинающиеся после pos. Таким образом, передав 0 как второй аргумент, мы ищем строку, которая начинается не позже 0, то есть в начале. Обычный же find продолжит искать подстроки далее начала.
👀3
Рассмотрим следующий код, в котором определен конструктор копирования для производного класса:
#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