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