Маша С++
104 subscribers
54 photos
2 files
65 links
Учу C++ и не только вместе с вами, мои котятки)
Download Telegram
Интересная особенность библиотечных объектов функций заключается в том, что реализация гарантирует их работоспособность с указателями. Вообще, результат сравнения (<, >) двух несвязанных указателей не определен. Но мы можем захотеть отсортировать н-р контейнер указателей по их адресу. Утверждается, что например сравнивать их на < нельзя, а вот std::less гарантирует верную реализацию (не знаю как):

#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
#include <string>

int main()
{
std::string s1, s2;
std::vector<std::string*> vp;

vp.push_back(&s1);
vp.push_back(&s2);
vp.push_back(new std::string("memory leak"));

auto vp2 = vp; // copy

std::sort(vp.begin(), vp.end(), [](std::string* l, std::string* r) {
return l < r; // непредсказуемый результат
});

auto print_vector_pointers = [&](std::vector<std::string*> v){
for (auto p : vp) {
std::cout << (size_t)p << " ";
}
std::cout << std::endl;
};

print_vector_pointers(vp);

std::sort(vp2.begin(), vp2.end(),
std::less<std::string*>()); // ok

print_vector_pointers(vp2);

return 0;
}


На самом деле у меня получились одинаковые результаты, но это не значит, что первый вариант можно использовать.

Более того, ассоциативные контейнеры используют для упорядочивания своих элементов std::less, поэтому можно определить set из указателей или map с указателем в качестве ключа. А еще std::sort начиная с C++20 использует std::less, а не operator< для сортировки.

Вывод: иcпользуйте std::less и std::greater - так короче и безопасней.
👍1
Как поймать исключение в другом потоке и передать в текущий?
Использовать std::exception_ptr.
Как видим получить указатель на текущее исключение (даже если мы ловим все с помощью ...), можно вызовом std::current_exception.

Возбудить исключение по указателю можно так:
        if (eptr)
std::rethrow_exception(eptr);
🔥6
std::size - это в некотором роде универсальная функция, которая вернет размер переданного интервала.
Ей можно передать любой контейнер, std::string или std:;string_view - для них просто вызовется size().
Также можно передать как C-style массив, так и std::array.
Вобщем, всеядная вещь. Доступна с C++17

#include <iostream>
#include <vector>
#include <list>
#include <array>

int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
std::cout << std::size(v) << std::endl; // 5

std::list<std::string> list = {"a", "b", "c"};
std::cout << std::size(list) << std::endl; // 3

std::string s = "abc";
std::cout << std::size(s) << std::endl; // 3

std::string_view sv{"Look, ma, I'm modern guy!"};
std::cout << std::size(sv) << std::endl; // 25

std::array<int, 4> sa = {1, 2};
std::cout << std::size(sa) << std::endl; // 4

int a[]{ 1, 2, 3, 4, 5 };
// Старый сишный способ
std::cout << (sizeof(a) / sizeof(a[0])) << std::endl; // 5
// Новый нормальный
std::cout << std::size(a) << std::endl; // 5

// Для массива символов вернет c \0
const char carr[] = "12345";
std::cout << std::size(carr) << std::endl; // 6 не 5!

return 0;
}
👍1
Моя скромная коллекция книг по C++, делитесь вашей в комментах 😊
#books
🔥51👍1
Продолжаем с лямбдами
#lambda
👍2🔥1
👍2
Про глобальные константы ч1.

Казалось бы, что глобальные константы - это такая почти каждодневная вещь и потому в плюсах с этим все должно быть очень просто. Мы часто пишем const int MAX_SOMETHING вместо скажем 1024, чтобы сделать наш код чище. Однако в реальности определение константы в плюсах может приводить к неожиданному поведению. Но давайте к практике.

Чтобы определить (глобальную) константу типа X мы можем написать:
const X x;

Если у X есть конструктор по умолчанию, то все отработает отлично, если мы проделаем это в единственном .cpp. Но что будет, если добавить такое определение в заголовок и затем включить в два .cpp файла (см картинку)? Такой случай скомпилируется, но это не определит глобальную константу!

Вместо этого будет 2 глобальные константы. Как так, спросите вы? Дело в том, что директива препроцессора #include тупо вставляет код заголовка в каждый .cpp файл. Так что после работы препроцессора, каждый .cpp файл будет содержать строку const X x;. То есть у каждого файла своя копия x.

В чем тут проблема?
- порядок инициализации не определен (не всегда проблема)
- тратится больше памяти
- конструктор вызывается дважды (и если он что-то делает, то мы возможно этого не хотели).

А правда ли в последнем пункте? Давайте проверим, вставив вывод в конструктор X:
class X
{
public:
X(){ std::cout << "X constructed\n"; }
};

Если у нас есть 2 .cpp, то вывод будет такой:
X constructed
X constructed

Конструктор вызвался дважды 😱.
(если что, то X const x и const X x - одно и тоже)
Про глобальные константы ч2.

Самый простой способ исправить проблему с глобальными константами выше - это использовать inline переменные из C++17.
Мы просто напишем вот так:
inline const X x;

Это скажет компилятору не определять объект в каждом файле, а поработать в паре с линкером и положить только один объект в двоичные файлы (как мы и хотели).

При этом inline в данном контексте может запутать, так как inline функции встраиваются в код, а inline переменные как-будто бы нет.

Как результат, конструктор вызовется один раз:
X constructed
.
1
2
Про глобальные константы ч3.

Константы внутри класса, объявленные как static, имеют ту же область видимости, что и глобальные константы и inline также упростил их определение в C++17.

До С++17 мы использовали весьма унылый паттерн, объявляя static в определении класса (заголовке) и определяя в единственном cpp файле (обычно для класса):

// заголовок
class MyClass
{
static const std::string MUFFIN_NAME;
};

// cpp (единственный)
const std::string MyClass::MUFFIN_NAME = "Rum baba";


Используя inline, мы можем объявить и определить статическую константу в классе одновременно:
// заголовок
class MyClass
{
static inline const std::string MUFFIN_NAME = "Rum baba";
};

// cpp - ничего не надо
3👌1
Виртуальные функции распознаются во время выполнения, только если вызов осуществляется через указатель или ссылку. В этом случае статический тип отличается от динамического.

#include <iostream>
#include <string>

struct Base
{
virtual void run(const std::string & f) const
{
std::cout << "Base::run() " << f << std::endl;
}
virtual ~Base() = default;
};

struct Derived : public Base
{
virtual void run(const std::string& f) const override
{
std::cout << "Derived::run() " << f << std::endl;
}
};

void f_pointer(const Base* b)
{
b->run("pointer");
}

void f_reference(const Base& b)
{
b.run("reference");
}

void f_value(const Base b)
{
b.run("value");
}

int main()
{
Derived d;

f_pointer(&d); // Derived::run() pointer
f_reference(d); // Derived::run() reference
f_value(d); // Base::run() value

return 0;
}


Как видим, при вызове по значению, будет использован статический тип, то есть вызовется метод того объекта, тип которого имеет переменная.
2
Про глобальные константы ч4.

До С++17 для создания одной константы можно использовать ключевое слово extern, такой код мы пишем в заголовке:

extern const X x;


Код выше с extern - это объявление, а не определение. А вот с inline это было определение. Объявление говорит, что где-то есть переменная с типом X.

Не смотря на то, что C++ требует уникальное определение объекта, объявлений может быть несколько.

Для того, чтобы использовать x, нам нужно где-то его определить. Это обычно используется в одном из .cpp файлов, в каком именно - решать вам, но лучше в том, где это будет иметь больше смысла.

Так как в заголовке только объявление, то и конструктор не принимает параметров (так как он там не вызывается). Вместо этого конструктор принимает параметры в одном из .cpp.

Важно: определение должно быть точно одно. Если определения нет, то будет "undefined external symbol", если больше одного, то - "duplicate external symbol".
Есть такой код стайл, например в том же Гугле, писать имя константы с k.
Например, так:
const int kDaysInAWeek = 7;

Всегда жутко это бесило, но стало интересно, откуда k? Первый вариант, который пришел в голову, что это константа (ведь Брин русский 😊). Гуглежь показал, что это не так далеко от правды.
На SO пишут, что в ранее популярной (и сейчас встречающейся, привет MS) венгерской нотации в начале имени переменной пишут сокращенный тип, например:
int iSize;
bool bBusy;
void *pFoo; // pointer
char chInitial;

Если же взять для имени константы c, то можно спутать с char или count. Кто-то умный предложил взять k, тк в немецком "konstant" это внезапно константа. Так что к могла бы и из русского придти)

P.S. я бы дописывала в начало const: const int constSize = 10;
Среднее арифметическое двух беззнаковых - тема частая, самый канонически пример - это бинарный (двоичный) поиск:
unsigned mid(unsigned low, unsigned high)
{
return (low + high) / 2;
}


Но тут есть такая проблемка: результат может переполниться. Например, если целые 32-битные, то среднее mid(0x80000000U, 0x80000000U) вернет 0. Не то, что мы бы хотели.

В случае, если мы знаем какое число больше (а мы часто знаем), среднее можно вычислить без переполнения так:

unsigned mid(unsigned low, unsigned high)
{
return low + (high - low) / 2;
}


Есть и другие формулы, для некоторых даже есть патент.
👍6🔥2
А сегодня, детишки, я расскажу вам сказку про С++ паттерн под названием Return Type Resolver (переведете сами).

Но давайте начнем с проблемы (вот бы все так делали).
Допустим, нам нужно написать 2 функции конвертирования строки в число, одну для int, вторую для float, пусть так:
int from_string(const char *str) { return std::stoi(str); }
float from_string(const char *str) { return std::stof(str); } // error


Но вот беда в том, что только по типу возвращаемого значения функцию перегрузить нельзя. Ну и ладно, назовем функции по разному и пофиг, хнык-хнык.

Но мы же в сказке и добро должно победить! Решение - параметризовать оператор приведения типа, например вот так:
class from_string
{
const string m_str;

public:
from_string(const char *str) : m_str(str) {}
template <typename type>
operator type(){
if constexpr(is_same_v<type, float>) return stof(m_str);
else if (is_same_v<type, int>) return stoi(m_str);
}
};

int n_int = from_string("123");
float n_float = from_string("123.111");


Алярм: требует C++17.
👍7🔥2🤔1
Если вы думаете, что паттерн Return Type Resolver из предыдущего поста бесполезный и плохой, то возможно вас удивит, что он может быть использовать для реализации nullptr, чтобы выводить нулевой указатель корректного типа в зависимости от типа переменной, к которой nullptr присваивается.

Простой вариант реализации мог бы выглядеть так:
struct nullptr_t 
{
void operator&() const = delete; // Can't take address of nullptr

template<class T>
inline operator T*() const { return 0; }

template<class C, class T>
inline operator T C::*() const { return 0; }
};

nullptr_t nullptr;


А вот пример реализации nullptr из llvm (впрочем, код очень старый)
1
Как все мы знаем, protected используется, чтоб предоставить наследникам доступ к защищенным членам, но закрыть от общего доступа. Но всегда ли производный класс может обратиться к protected членам? Давайте посмотрим на код ниже:
class Base
{
protected:
int _protMember;
};

class Derived : public Base
{
private:
void f(Derived& d);
void f(Base& b);
int _privMember;
};

void Derived::f(Derived& d)
{
d._privMember = 0;
d._protMember = 0; // OK
}

void Derived::f(Base& b)
{
b._protMember = 0; // Не скомпилится
}

В первой реализации функции f() класс-наследник Derived спокойно может обратиться к защищенному и унаследованному полю _protMember через ссылку на Derived.
А вот во втором методе f() доступ к тому же полю через ссылку на Base уже не доступен - код не скомпилится.

Таким образом, производный член класса (или его friend) может обратиться к защищенным членам базового класса только через объект производного.
Немного про закрытое (private) наследование.
Спецификатор доступа наследования никак не влияет на возможность производного класса обратиться к членам его собственного прямого базового класса. Приватное наследование на запретит доступ непосредственным (!) детям к protected, любое наследование не разрешит доступ к private. Класс сам определяет права доступа наследникам:
class Base
{
protected:
int _protMember;
};

class PublicDerived : public Base
{
public:
int f() { return _protMember; } // OK
};

class PrivateDerived : private Base
{
public:
int f() { return _protMember; } // OK
};


Это все очевидно, но интересен вопрос, а что если добавить в цепочку наследования еще классов с разными доступами наследования:
class DerivedFromPublic : public PublicDerived
{
public:
int f2() { return _protMember; } // OK
};

class DerivedFromPrivate : public PrivateDerived
{
public:
int f2() { return _protMember; } // NOT OK
};

Как видим, protected так и остался доступен для второго уровня наследника при public наследовании, а вот исправить public наследованием от private наследования уже нельзя - protected член стал безвозвратно недоступным из-за первого private наследования.