Wednesday, December 4, 2019

Менеджер данных (одиночка) в си++.

В программировании часто приходится решать задачу по управлению данными, в числе которых могут быть игровые объекты, текстуры, состояния, компоненты и т.д. Обычно нам нужно их где-то хранить, иметь возможность их создавать, добавлять в хранилище, удалять из хранилища, уничтожать, обрабатывать. Наши данные представлены собственными классами (возможно со своей иерархией наследования) и логично предположить, что для управления этими классами нам понадобится новый отдельный класс со своими методами призванными решать задачи по управлению нашими данными и назовем мы его например IntManager.
 
Данные (int) будем хранить в векторе. Методами для управления пусть будут метод для добавления данных в вектор и метод для печати количества элементов в векторе. Дальше нам понадобятся конструктор и деструктор и тут мы должны для себя понять, нужны ли нам несколько таких менеджеров одновременно. Очевидно, что нет, мы не хотим создать условия, при которых можем получить одновременный доступ из нескольких мест к одним и тем же данным. Значит нам нужно придумать способ наличия в системе только одного менеджера и такой способ есть – использовать паттерн Одиночка (singleton). Этот подход определяет несколько правил, следуя которым мы решим нашу задачу.
 
Сначала нам нужно закрыть доступ к конструктору и деструктору(а также к копирующему конструктору и к копирующему оператору присваивания), а обращение к объекту нашего менеджера осуществлять через статический метод Instance(), который будет создавать при необходимости объект нашего менеджера, а при его наличии возвращать статический указатель на него. Статический метод позволит нам обращаться к этому методу без наличия объекта класса, а статический указатель будет храниться в статической памяти, т. е. независимо. Не забываем, что статические члены класса нужно проинициализировать за пределами класса, в нашем случае нулем. Про статическую память можно почитать в одном из моих предыдущих постов.

#include <iostream>
#include <vector>

class IntManager {
public:
    static IntManager *Instance() {
        if (s_pInstance == nullptr) {
            s_pInstance = new IntManager();
            return s_pInstance;
        }
        return s_pInstance;
    }


    void AddInt(int _i){
        ints.push_back(_i);
    }

    void GetSize(){
        std::cout << "Size: " << ints.size() << std::endl;
    }

private:
    IntManager() = default;
    virtual ~IntManager() = default;

    IntManager(const IntManager &);
    IntManager &operator=(const IntManager &);

    static IntManager *s_pInstance;
    std::vector<int> ints;
};

IntManager* IntManager::s_pInstance = nullptr;

int main() {
    IntManager::Instance()->AddInt(1);
    IntManager::Instance()->AddInt(2);
    IntManager::Instance()->GetSize();

    return 0;
}

Output:
>>Size: 2

Перегуд В.

Thursday, November 28, 2019

Вариативные шаблоны в си++

Никогда бы не подумал, что мне эта штука будет полезна, но был очень сильно не прав. Итак, вариативные шаблоны, казалось бы, растут они из вариативных функций и нужно сначала разобраться с ними, но нет, делать этого мы не будем. Давайте сразу к шаблонам, смотрите, шаблоны — это способность унифицировать тип, например, подставлять стандартные типы, но еще лучше, что мы можем подставлять пользовательские типы, конструкторам которых нужны разные наборы параметров для конструирования. Ничего не напоминает? Да, разные наборы параметров в конструкторах это и есть вариативные функции с переменным числом аргументов. Такой инструмент позволяет нам создавать, например, простые функции-фабрики объектов, передавая в них тип в качестве параметра шаблона и параметры для конструктора в качестве переменного числа аргументов. И если допустить, что создаваемые объекты нам нужны в единичном количестве, то искать мы их можем в словаре, где значением является созданный объект, а ключом будет его тип, вернее указатели на них. Это лучше, чем вектор с поиском по индексу. Если понадобится хранение нескольких объектов одного и того же типа, то нужно будет вводить “айдишки” и создавать более сложную фабрику, но это отдельный разговор.

Пример ниже рассматривает создание примитивной компонентной системы, в которой мы создаем компоненты с помощью вариативного шаблона AddComponent.
  
#include <iostream>
#include <vector>
#include <map>

class Component {
public:
    virtual void init() = 0;
};
 
class Transform : public Component {
public:
    Transform(int _x, int _y) : x{_x}, y{_y} {}

    void init() override {
        std::cout << "transform component ready" << std::endl;
    }

private:
    int x, y;
};

class Image : public Component {
public:
    Image(const std::string& _file) : file{_file} {}

    void init() override {
        std::cout << "image component ready" << std::endl;
    }

private:
    std::string file;
};

std::map<const std::type_info *, Component *> componentTypeMap;

template<typename T, typename... TArgs>
T *AddComponent(TArgs &&... args) {
    T *newComponent = new T(std::forward<TArgs>(args)...);
    componentTypeMap[&typeid(*newComponent)] = newComponent;
    return newComponent;
}

template<typename T>
T *GetComponent() {
    std::cout << "return [" << typeid(T).name() << "] component";
    return static_cast<T *>(componentTypeMap[&typeid(T)]);
}

int main() {
    std::vector<Component *> entity;
    entity.push_back(AddComponent<Transform>(0, 0));
    entity.push_back(AddComponent<Image>("image.png"));
    for(auto component : entity){
        component->init();
    }
    GetComponent<Image>();
    entity.clear();
    return 0;
}

Output:
>>transform component ready
>>image component ready
>>return [5Image] component

Tuesday, November 26, 2019

Обратные вызовы и лямбда выражения в си++

Отец плюсов говорит - “Не совершенствуйте язык, а совершенствуйте его применение”. Давайте так и поступим. Нам пригодилось бы передавать функцию в другую функцию в качестве параметра, т.е. как переменную. Еще много чего, но давайте потерпим. Оказывается, так можно. Как и любая переменная, функция имеет свой тип, т.е. область в памяти, определенного размера, достаточного для хранения информации о том, как выглядит наша функция (что возвращает и какие параметры принимает). Имея указатель на эту область, мы можем выполнить наши ранее описанные задачи. Изменить значению такого указателя, можно просто присвоив ему имя функции, которая нам подходит. А выполнить функцию через указатель можно с помощью добавления к имени указателя круглых скобок.

Для примера функция void foo() имеет тип void(*)(), чтобы объявить и проинициализировать указатель такого типа, нужно написать void (*fPtr)() = nullptr, где fPtr и есть имя указателя. Далее можно отследить по коду. Погодите, мы углубились в язык, но совсем забыли про применение. Где же можно применить такие чудо-возможности? Например, для хранения коллекции разных действий (функций), или, например, для получения обратного вызова. Обратный вызовэто вызов функции, которая в свою очередь должна вызвать функцию, полученную в качестве параметра. Часто это может быть полезно, например, запуская метод click() для кнопки, и передавая ему действие, которое должно быть выполнено при нажатии на кнопку.

Еще одно полезное применение можно получить, для передачи в коллекцию действия, которое мы хотим выполнить над каждым элементом коллекции. В стандартной библиотеке есть множество вспомогательных алгоритмов, например, сортировка, которая уже реализует метод сравнения элементов для стандартных типов. Но что делать с пользовательскими типами? Мы сами можем создать метод сортировки и передать его в качестве функции. Но у такого решения есть один недостаток, функция имеет свое пространство имен, что требует передачу в нее других параметров, это часто не удобно, т.к. к примеру метод сортировки, ждет только определенные параметры, которые зафиксированы в типе (функции) параметра и если мы туда передадим еще что-то, то сломаем логику. Решить эту проблему призваны лямбда-выражения, они же лямбда-функции, они же замыкания. Это специальный синтаксис, позволяющий захватить внешние для самой лямбды переменные и использовать их внутри себя, оставляя привычным (для принимающей стороны) передачу параметров. С технической стороны, лямбды требуют отдельного рассмотрения, т.к. они имеют очень специфический синтаксис. Особенно в контексте работы со стандартной библиотекой. Как вариант, мы можем писать лямбду на месте самого параметра, принимающего функцию, что часто очень наглядно выражает логику программы. На текущий момент мы говорим о них, как о еще одной возможности передавать действие в качестве параметра (смотрите пример кода) и пока на этом остановимся.
 
#include <iostream>
#include <vector>

class Button {
public:
    explicit Button(void (*_fn)()) : fn{_fn} {    }

    void click() {
        fn();
    }

    void setFn(void (*_fn)()) {
        fn = _fn;
    }

private:
    void (*fn)() = nullptr;
};

static void foo1() {
    std::cout << "foo1 called!" << std::endl;
}

static void foo2() {
    std::cout << "foo2 called!" << std::endl;
}

void foo3(void (*_l)()) {
    _l();
}

int main() {

    typedef void(*CB)();
    //std::vector<void (*)()> calls = {foo1, foo2};
    std::vector<CB> calls = {foo1, foo2};

    for (auto i : calls) {
        i();
    }
    //Output:
    //>>foo1 called!
    //>>foo2 called!

    auto b1 = Button(foo1);
    auto b2 = Button(foo2);

    std::vector<Button> btns = {b1, b2};

    for (auto b : btns) {
        b.click();
    }
    //Output:
    //>>foo1 called!
    //>>foo2 called!

    btns[0].setFn(foo2);
    btns[1].setFn(foo1);

    for (auto b : btns) {
        b.click();
    }
    //Output:
    //>>foo2 called!
    //>>foo1 called!

    foo3(foo2);
    //Output:
    //>>foo2 called!

    foo3([]() -> void {
        std::cout << "lambda called!";
    });
    //Output:
    //>>lambda called!

    return 0;
}

Перегуд В.

Friday, November 22, 2019

Многомерные массивы в си++

Мы знаем, что по причине обратной совместимости с си, массивы в си++ представлены двумя вариантами, базовым и из стандартной библиотеки. Базовый вариант имеет известные недостатки, поэтому сегодня мы поговорим о варианте из стандартной библиотеки в контексте многомерных массивов. В частности, мы рассмотрим создание и работу с двумерным массивом с использованием классов std::array и std::vector.
 
#include <iostream>
#include <iomanip>
#include <vector>
#include <array>

Двумерный массив удобно представлять в виде массива строк, в котором каждая строка также является массивом элементов array[row][col]. Если представить такой массив графически, то получим таблицу, со строками по горизонтали и столбцами по вертикали. Обход элементов, можно осуществить с помощью вложенного цикла.

//access
template <typename T>
void printArray(T _array) {
    std::cout << "-------------------------------" << std::endl;
    int i = 1;
    for (const auto &row_in_arr : _array) {
        std::cout << "row" << i++ << ": ";
        for (const auto &col_in_row : row_in_arr) {
            std::cout << "[ " << std::setw(2) << std::setfill('0') << col_in_row << " ]";
        }
        std::cout << std::endl;
    }
}

Класс array служит для организации данных в группу элементов фиксированной длины, имеет метод возвращающий размер группы size(), а также метод доступа к элементу по индексу, проверяющий выход за границы массива at(index).
 
int main() {
    //array of arrays
    auto myArr1 = std::array<std::array<int, 5>, 5>();
   
    printArray(myArr1);
    //-------------------------------
    //row1: [ 00 ][ 00 ][ 00 ][ 00 ][ 00 ]
    //row2: [ 00 ][ 00 ][ 00 ][ 00 ][ 00 ]
    //row3: [ 00 ][ 00 ][ 00 ][ 00 ][ 00 ]
    //row4: [ 00 ][ 00 ][ 00 ][ 00 ][ 00 ]
    //row5: [ 00 ][ 00 ][ 00 ][ 00 ][ 00 ]

    //filling
    int x = 1;
    for (auto &row_in_arr : myArr1) {
        for (auto &col_in_row : row_in_arr) {
            col_in_row = x++;
        }
    }
   
    printArray(myArr1);
    //-------------------------------
    //row1: [ 01 ][ 02 ][ 03 ][ 04 ][ 05 ]
    //row2: [ 06 ][ 07 ][ 08 ][ 09 ][ 10 ]
    //row3: [ 11 ][ 12 ][ 13 ][ 14 ][ 15 ]
    //row4: [ 16 ][ 17 ][ 18 ][ 19 ][ 20 ]
    //row5: [ 21 ][ 22 ][ 23 ][ 24 ][ 25 ]
   
    //size
    std::cout << "size: " << myArr1.size() * myArr1.at(0).size() << std::endl;
    //size: 25
   
    //indexes
    std::cout << "row:1 col:1 " << "[ " << myArr1[0][0] << " ]" << std::endl;
    std::cout << "row:5 col:5 " << "[ " << myArr1[4][4] << " ]" << std::endl;
    //row:1 col:1 [ 1 ]
    //row:5 col:5 [ 25 ]
   
    //range
    try {
        std::cout << "row:6 col:6 " << ">>> " << myArr1.at(5).at(5) << std::endl;
    } catch (std::out_of_range &e) {
        std::cout << "out_of_range!!!" << std::endl;
    }
    //row:6 col:6 >>> out_of_range!!!

Класс vector дополняет вышесказанное возможностью изменять размер массива, например при помощи метода добавления нового элемента в конец массива push_back(obj). Так как при использовании этого метода происходит копирование, можно использовать метод emplace_back(obj), конструирующий новый объект.
 
    //vector of vectors
    auto myArr2 = std::vector<std::vector<int>>(5, std::vector<int>(5, 0));
   
    printArray(myArr2);
    //-------------------------------
    //row1: [ 00 ][ 00 ][ 00 ][ 00 ][ 00 ]
    //row2: [ 00 ][ 00 ][ 00 ][ 00 ][ 00 ]
    //row3: [ 00 ][ 00 ][ 00 ][ 00 ][ 00 ]
    //row4: [ 00 ][ 00 ][ 00 ][ 00 ][ 00 ]
    //row5: [ 00 ][ 00 ][ 00 ][ 00 ][ 00 ]

    //add row
    myArr2.push_back(std::vector<int>(4,1));
    //construct row
    //myArr2.emplace_back(4,1);
   
    //add element
    myArr2.at(5).push_back(10);
   
    printArray(myArr2);
    //-------------------------------
    //row1: [ 00 ][ 00 ][ 00 ][ 00 ][ 00 ]
    //row2: [ 00 ][ 00 ][ 00 ][ 00 ][ 00 ]
    //row3: [ 00 ][ 00 ][ 00 ][ 00 ][ 00 ]
    //row4: [ 00 ][ 00 ][ 00 ][ 00 ][ 00 ]
    //row5: [ 00 ][ 00 ][ 00 ][ 00 ][ 00 ]
    //row6: [ 01 ][ 01 ][ 01 ][ 01 ][ 10 ]

    //access
    std::cout << "row:1 col:1 " << "[ " << myArr2[0][0] << " ]" << std::endl;
    std::cout << "row:6 col:5 " << "[ " << myArr2.at(5).at(4) << " ]" << std::endl;
    //row:1 col:1 [ 0 ]
    //row:6 col:5 [ 10 ]

    return 0;
}

Нужно помнить, что индексы элементов в массиве начинаются с 0 (ноль).
 
Чем же отличаются std::array и std::vector? Тем, что массив хранит свои элементы в стеке, а вектор в куче. Поэтому массив быстрее, т.к. не тратит время на аллокацию и деаллокацию.

Перегуд В.

Tuesday, November 19, 2019

Владение ресурсами в си++

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

С++11 ввел семантику перемещения, которая при наличии rvalue значения, позволяет его перемещать явно или не явно. Не явно это происходит, при оптимизации во время компиляции. Явно перемещение можно задать с помощью std::move(obj), для объектов которые поддерживают перемещение в своих типах. Поддержку перемещения можно обеспечить, явно определив конструктор перемещения и перемещающий оператор присваивания. Таким образом правило трех превратилось в правило пяти. Если мы хотим запретить копирование, то можем указать запрет на автоматическое создание с помощью =delete для конструктора копирования и копирующего оператора присваивания.
 
#include <iostream>

class Box {
public:
    //constructor
    explicit Box(int _number) : number{_number} {
        bigData = new int(number);
    }

    //destructor
    ~Box() {
        delete bigData;
    }

    //copy constructor
    Box(const Box &other) {
        number = other.number;
        bigData = new int(0);
        *bigData = *other.bigData;
    }

    //copy assign operator
    Box &operator=(const Box &other) {
        if (this == &other)
            return *this;
        number = other.number;
        delete bigData;
        bigData = new int(0);
        *bigData = *other.bigData;
        return *this;
    }

    //move constructor
    Box(Box &&other) noexcept {
        number = other.number;
        bigData = other.bigData;
        other.number = 0;
        other.bigData = nullptr;
    }

    //move assign operator
    Box &operator=(Box &&other) noexcept {
        if (this == &other)
            return *this;
        number = other.number;
        delete bigData;
        bigData = other.bigData;
        other.number = 0;
        other.bigData = nullptr;
        return *this;
    }

public:
    int *bigData = nullptr;
    int number;
};

Стандартная библиотека си++ предлагает специальные типы для решения задачи владения, так называемые умные (smart) указатели. Рассмотрим основные из них, это уникальный указатель std::unique_ptr<T> и указатель совместного владения std::shared_ptr<T>. При их использовании нет необходимости в new и delete. Также умные указатели возвращают bool при проверках. В отличие от сырых (raw) указателей, умные указатели отвечают за очистку памяти при их удалении.

int main() {
    Box box{10}; // constructor
    Box box1{box};  // copy constructor
    Box box2 = box1; // copy assign operator
    Box box3{std::move(box2)}; // move constructor
    Box box4 = std::move(box3); // move assign operator

    std::cout << (box3.bigData ? "filled" : "empty") << std::endl;
    std::cout << (box3.number ? "10" : "0") << std::endl;
    std::cout << (box4.bigData ? "filled" : "empty") << std::endl;
    std::cout << (box4.number ? "10" : "0") << std::endl;

    std::unique_ptr<Box> uPtr1 = std::make_unique<Box>(std::move(box4));
    std::cout << (uPtr1 ? "true" : "false") << std::endl;
    std::unique_ptr<Box> uPtr2 = std::move(uPtr1);
    std::cout << (uPtr1 ? "true" : "false") << std::endl;

    std::shared_ptr<Box> sPtr1 = std::make_shared<Box>(55);
    std::shared_ptr<Box> sPtr2 = sPtr1;
    std::cout << *sPtr1->bigData << std::endl;
    std::cout << *sPtr2->bigData << std::endl;

    return 0;
}

Output:
>>empty
>>0
>>filled
>>10
>>true
>>false
>>55
>>55

Перегуд В.

Friday, November 15, 2019

Абстрактные классы (интерфейсы) в си++

Любой объект или сущность в природе имеет уровень абстракции. Это удобно и позволяет переходить от общего к деталям и наоборот. Например, “стул” — это конкретное описание предмета, но мы также можем говорить о нем, как о “мебели” или к примеру, как об “изделии из дерева”. Исходя из этого мы можем сказать, что стул — это также и элемент мебели, еще раз – стул является элементом мебели. В свою очередь стулья могут делиться на офисные, кухонные, и т.д., что в свою очередь вырастает в довольно запутанную иерархию отношений.  В нашем случае нам помогают с этим справиться классы и наследование, мы говорили об этом, когда рассматривали полиморфизм. Животные как общий уровень абстракции и кот с собакой, как конкретный уровень абстракции. К примеру общий уровень нам нужен как тип контейнера, в котором мы храним конкретные типы.

Как правило, когда мы работаем с иерархией классов нам нужны объекты конкретных типов, а не общих. В рассмотренном примере нам были нужны объекты конкретно собаки и кота. Но если бы мы рассматривали животных, рыб и людей, как конкретные типы формы жизни на нашей планете, то животные были бы конкретным типом, и мы бы создавали такие объекты.  Более того, если мы создаем в базовом классе чисто виртуальные функции, которые обязуемся реализовать в наследниках, то физически не можем создавать объекты такого класса. Вот и получается, что базовый класс для таких наследников является абстрактным, и служит лишь для доступа к наследующим его конкретным классам, и мы называем его интерфейсом.
 
Как пульт дистанционного управления является интерфейсом между человеком и телевизором. Пультов много разных по форме, способу ввода информации и т.д. но все они должны предоставлять для пользователя кнопки с цифрами, для ввода номеров каналов. Этими кнопками служат наши чисто виртуальные методы. В современных языках программирования (Java), есть специальное ключевое слово “Interface“, служащее специально для этих целей, что облегчает узнавание таких классов в коде.  Давайте рассмотрим пример с такой иерархией.

           [__Shape__]       [_IDrawable_]      [_IMovable_]
          ^                 ^          ^            ^            ^
         /                     \       /           /             /
  [_Triangle_]        [_Circle_]         /             /
         \_____________________/________/

У нас есть базовый класс Shape, от которого мы наследуем классы Triangle и Circle. У Фигуры есть приватные поля и публичные методы, которые наследуются. У Круга также есть поле и метод. Также у нас есть два интерфейса. IDrawable, который наследуется Треугольником и Кругом, и IMovable, который наследуется только Треугольником. Таким образом мы хотим гарантировать то что оба наследника рисуются, т.е. переопределяют метод draw(). И Треугольник может двигаться, переопределяя метод move().

#include <iostream>
#include <vector>
#include <string>

class IDrawable {
public:
    virtual void draw() = 0;
};
class IMovable {
public:
    virtual void move() = 0;
};

class Shape {
public:
    Shape(int _x, int _y) : x{_x}, y{_y} {}
    virtual ~Shape() = default;
    int getX() const {return x;}
    int getY() const {return y;}
private:
    int x;
    int y;
};

class Triangle : public Shape, public IDrawable, public IMovable{
public:
    Triangle(int _x, int _y) : Shape(_x, _y) {}
    ~Triangle() override = default;
    void draw() override {std::cout << "Triangle draw\n";}
    void move() override {std::cout << "Triangle move\n";}
private:
};

class Circle : public Shape, public IDrawable {
public:
    Circle(int _x, int _y, int _r) : Shape(_x, _y), r{_r} {}
    ~Circle() override = default;
    void draw() override {std::cout << "Circle draw\n";}
    int getR() const {return r;}
private:
    int r;
};

Теперь для того чтобы не создавать каждый раз новый контейнер, определенного типа (например, одного из интерфейсов или базового класса), а работать с одним, нам нужен инструмент для перехода по иерархии типов, для вызова методов этих типов или доступа к данным. Дело в том, что имея например, контейнер Фигур, мы можем иметь доступ только к членам этого типа. И хотя в контейнере хранятся Круги, нам не доступен Радиус (int r) этих объектов. Но так как Круг является Фигурой (по нашей иерархии), мы можем изменить тип объекта из Фигуры в Круг, тем самым получив доступ к его членам. В этом нам поможет динамическое приведение типов dynamic_cast<T>(obj), принимающий в качестве параметра объект obj, который мы хотим привести к типу T.

int main() {
    Triangle tri1(0, 0);
    Circle cir1(1, 1, 1);
    std::vector<IDrawable *> shapes = {&tri1, &cir1};

    for (auto i_asIDrawable : shapes) {
        i_asIDrawable->draw();
        if (auto i_asShape = dynamic_cast<Shape *>(i_asIDrawable)) {
            std::cout << "x: " << i_asShape->getX() << std::endl;
            std::cout << "y: " << i_asShape->getY() << std::endl;
        }
        if (auto i_asCircle = dynamic_cast<Circle *>(i_asIDrawable)) {
            std::cout << "r: " << i_asCircle->getR() << std::endl;
        }
        if (auto i_asIMovable = dynamic_cast<IMovable *>(i_asIDrawable)) {
            i_asIMovable ->move();
        }
    }
    return 0;
}

Output:
>>Triangle draw
>>x: 0
>>y: 0
>>Triangle move
>>Circle draw
>>x: 1
>>y: 1
>>r: 1

К сожалению, использовать динамическое приведение очень дорого, т.к. создается временная копия объекта с новым типом. И считается плохим архитектурным решением. Поэтому часто, в угоду производительности,  интерфейсы объединяют в базовые классы, что усложняет понимание кода.
 
Перегуд В.

Thursday, November 14, 2019

JSON for modern C++

//https://github.com/nlohmann/json

#include <iostream>
#include <iomanip>
#include <nlohmann/json.hpp>
#include <fstream>

using json = nlohmann::json;

int main() {
    // a JSON text
    char text[] = R"(
     {
         "Image": {
             "Width":  800,
             "Height": 600,
             "Title":  "View from 15th Floor",
             "Thumbnail": {
                 "Url":    "http://www.example.com/image/481989943",
                 "Height": 125,
                 "Width":  100
             },
             "Animated" : false,
             "IDs": [116, 943, 234, 38793]
         },
        "Image1": {
             "Width":  800,
             "Height": 600,
             "Title":  "View from 15th Floor",
             "Thumbnail": {
                 "Url":    "http://www.example.com/image/481989943",
                 "Height": 125,
                 "Width":  100
             },
             "Animated" : false,
             "IDs": [116, 943, 234, 38793]
         }
     }
     )";

    // parse and serialize JSON
    json j_complete = json::parse(text);
    //std::cout << std::setw(4) << j_complete << "\n\n";

    //std::ifstream ifs("States.json");
    //json j = json::parse(ifs);
    //std::cout << std::setw(4) << j << "\n\n";

    for (const auto &item : j_complete.items()) {
        std::cout << item.key() << "\n";
        if(item.key() == "Image"){
            std::cout << "!!!!!!!!!!!!!!\n";
        }

        for (const auto &val : item.value().items()) {
            std::cout << "  " << val.key() << ": " << val.value() << "\n";
            if(val.key() == "Thumbnail"){
                for(const auto &xx : val.value().items()){
                    if(xx.key() == "Url"){
                        std::cout << "*****************\n";
                    }
                }
                std::cout << "================\n";
            }
            if(val.value() == 800){
                std::cout << "XXXXXXXXXXXXXX\n";
            }
        }

    }
}

Tuesday, November 12, 2019

Константы в си++

Не совсем по теме, но тоже есть слово “const”, поэтому коротко рассмотрим и этот вариант – constexpr. Если наша цель перф, то один из способов оптимизировать код, это произвести вычисления на этапе компиляции, а не во время выполнения программы. Это возможно, когда код содержит описание логики с помощью выражений, в составе которых все переменные и функции “известны” на момент компиляции или могут быть вычислены на этапе компиляции. Тема не простая и требует отдельного разговора.

Далее будем рассматривать const в классическом понимании – мы обещаем не изменять наши переменные и хотим, чтобы компилятор следил за этим.

Константные переменные. Объявление переменной с помощью const, требует ее инициализации во время объявления и запрещает ее изменение в дальнейшем. Попытки это сделать приведут к ошибкам. ( const int x = 0; )

Немного сложнее с указателями. При использовании const слева от типа ( const int* pX = &x; ), мы запрещаем изменение значения, на которое указывает указатель. При использовании const справа от типа ( int* const pX = &x; ), мы запрещаем изменение самого указателя. Кстати для указателей можно использовать оба варианта одновременно ( const int* const pX = &x; ).

Но для референсной ссылки будет работать только первый вариант ( const int& rX = x; ), когда мы запрещаем изменение значения, т.е. переменной, на которую указывает ссылка. И тут важно понимать, что при присвоении ссылке нового значения, мы не меняем ссылку, а присваиваем новое значение переменной, на которую указывает ссылка. Это нам пригодится при рассмотрении возврата из функций в контексте констант.

Константы в функциях. При возвращении функцией указателя или референсной ссылки, мы превращаем вызов такой функции в lvalue, т.е. в переменную, которая может стоять слева от знака присваивания и соответственно мы можем присвоить этой переменной новое значение тем самым изменив ее. Если нам нужно защититься от этого, то мы можем указать const перед типом возвращающего значения функции ( const int& foo(){} ). Внимание, дополнительно тут хотелось бы отметить, что нельзя возвращать указатель или ссылку на локальную переменную из тела функции, которая уничтожается при выходе из функции.

Теперь давайте рассмотрим параметры функции, мы помним, что по умолчанию в си++ передача параметров в функции происходит по значению, т.е. внутри функции происходит копирование данных. Если данные “тяжелые”, это может потребовать значительных затрат. Чтобы избежать копирования, мы можем передавать данные по указателю или ссылке, что в свою очередь ставит их под угрозу изменения. Чтобы их защитить мы можем использовать константные указатель или ссылку ( void foo(const int&){} ). Через константную ссылку на объект класса можно вызвать только константный метод.

Константы в методах. Так же, как и функции, методы могут возвращать и принимать константы. Но есть еще один дополнительный момент. Если мы хотим, чтобы метод НЕ менял поля класса, например это геттер, который возвращает значение поля, то можем это обеспечить с помощью ключевого слова const между списком параметров и телом метода( int getter() const {} ). Тут нужно добавить, что если мы используем в таком методе другие методы этого класса, они также должны гарантировать защиту данных.

Перегуд В.

Sunday, November 10, 2019

Статические переменные и функции в си++

Давайте рассмотрим по прядку все случаи использования статической памяти. Мы уже говорили о том, что статические переменные НЕ уничтожаются при выходе из своего пространства имен, хотя доступны только из него. Также мы знаем, что они инициализируются один раз, при объявлении (явно или не явно - значением по умолчанию), при последующих обращениях мы можем считывать и изменять их значение.
 
Первыми рассмотрим глобальные переменные и функции. Мы помним, что глобальные переменные и функции, доступны из любого места программы, и что самое важное в этом примере – из других файлов нашей программы, если мы разделим код на несколько файлов для удобства. Но если мы объявим их как статические (static), то тем самым сузим их видимость до текущего файла. И это может быть полезным, если мы хотим ограничить их видимость, например, в пределах библиотеки, которую создаем для других пользователей. Ведь когда мы подключаем сторонние библиотеки к своей программе, то получаем доступ к ее глобальным переменным и функциям, но не к тем, которые объявлены как статические.
 
Далее рассмотрим локальные переменные и функции. С ними все тоже самое, только видимость ограничивается не рамками файла, а тем пространством имен, в котором эти функции объявлены. Например, рамками внешней функции, по отношению к которой наши локальные члены являются внутренними.
 
#include <iostream>

void foo() {
    static int count = 0;
    count++;
    std::cout << "count is " << count << "\n";
}

int main() {
    for (auto i = 0; i != 3; i++) {
        foo();
    }
    return 0;
}

Output:
>>count is 1
>>count is 2
>>count is 3


До этого момента, мы говорили про переменные простых типов, которые также включают указатели. Т.е. указатели тоже могут быть статическими или автоматическими. Теперь давайте поговорим о пользовательских типах в контексте статической памяти.
 
Статические поля классов. Если поле класса объявлено как статическое, то это значит, что значение этого поля может быть сохранено и доступно для чтения и редактирования между объектами этого класса. И тут интересный момент, помните мы говорили про инициализацию статической переменной один раз при объявлении? Статическое поле нужно инициализировать за пределами класса. Так как оно должно существовать до появления объектов класса. Обращение к нему осуществляется через имя класса и оператор области видимости (::).
 
Статические методы классов. В свою очередь, если мы имеем статический метод класса, то для его вызова нам НЕ нужно создавать объект класса. Вызов такого метода осуществляется через имя класса и оператор области видимости (::). Это удобно для создания утилитарных классов с наборами общих вспомогательных методов. А также для других случаев, про которые мы поговорим в следующий раз.
 
Статические объекты классов. В этом случае ситуация схожа с переменными простых типов, не будем повторяться.

#include <iostream>

class Box {
public:
    Box(){
        count++;
        std::cout << "count is " << count << "\n";
    }
    ~Box() = default;

    static int count;
};

int Box::count = 0;

int main() {
    Box b1;
    Box b2;
    Box b3;

    return 0;
}

Output:
>>count is 1
>>count is 2
>>count is 3
Перегуд В.

Monday, November 4, 2019

Память в си++.


Сначала был бит. И в конечном итоге в памяти мы храним данные: записываем, считываем, освобождаем, скорость с которой мы это делаем, допустимый объем, все это здорово. Но мы будем говорить не об этом, мы поговорим о том, как бы нам было удобнее хранить и использовать данные.
-
Первым делом нам нужны имена для наших значений, и еще нам нужно разбить данные на кусочки, получаем переменные и их типы. Пока не будем углубляться в то, что типы могут быть простыми, составными и пользовательскими. Но заострим внимание на том, что сами по себе имена переменных, это тоже данные которые нам нужно хранить. Тут нам на помощь приходят пространства имен, которые в свою очередь определяют время жизни переменной. Классический пример это фигурные скобки, в рамках которых не может быть переменных с одним именем и в тоже время наши переменные не могут быть доступны за их рамками. 
-
Не знаю кто придумал функции, но это круто, мы можем делить код на блоки, организовывая выполнение выражений в удобную для человека форму, путем передачи в них параметров и получения из них результата и все это посредством наших переменных. Передача и возврат по значению, по ссылке, по указателю, это тоже все круто, но мы чуть про другое. Для удобства, функции могут иметь вложенные функции, из одних функций могут вызываться другие функции, функции могут передаваться в качестве параметров и это порождает иерархию пространств имен и переменных, за которой человеку сложно уследить. А еще хотелось бы экономно использовать память, где все это должно храниться. Учитывая, что только человек знает, какие переменные ему нужны, оставим на его совести их создание, но может тогда хотя-бы помочь ему освобождать память. И тут нам на помощь приходит первый вид переменных:
-
Автоматические переменные. Переменные, которые автоматически удаляются, выходя за пределы пространства, в котором были объявлены. Заметим, что они все еще доступны во вложенных пространствах, но могут попасть в тень переменных с таким же именем. Частным случаем автоматических переменных, являются глобальные переменные. Это переменные, которые объявлены в глобальном пространстве имен и создаются при старте программы и освобождается при завершении. Поэтому первой точкой входа при старте программы является глобальное пространство имен, а затем уже функция main, которая запускается первой среди определенных в этом пространстве функций. Так как такие переменные активно порождаются и освобождаются, тем самым фрагментируя память, для эффективного хранения и управления используют структуру данных - стек. И выделяют отдельную область памяти, которая тоже называется стек.
-
Отлично, а как же нам тогда общаться между функциями? Частично эту проблему решают глобальные переменные, но это не всегда удобно, глобальное пространство имен делает видимой такую переменную для всех функций. Часто это бывает необходимо, когда нам нужны общие для всех константы, но что, если мы хотим ограничить доступ к переменной в рамках конкретного пространства имен. Например, чтобы созданная в рамках какой-то функции, переменная не освобождалась при выходе из функции, а сохраняла свое значение и была доступна при последующих запусках этой функции. В этом случае нам поможет второй вид переменных: Статические переменные. Такие переменные объявляются с помощью ключевого слова static. Их инициализация происходит при первом обращении к ним и значение хранится в специальной области памяти, которая называется статической. Пользовательские типы немного сложнее в работе со статической памятью, но это отдельная тема для разговора.
-
Говоря об использовании и управлении памятью нельзя не сказать про указатели. И это как раз тот инструмент, который позволяет эффективно управлять памятью вручную. Во-первых, оперировать значениями может быть очень ресурсоемко, например, пользовательский тип может в себе содержать целую иерархию простых и пользовательских типов, что займет значительный объем памяти и работа с ним может значительно нагрузить систему. Если привести аналогию из жизни, то проще носить между инстанциями документы на груз, чем таскать с собой грузовой контейнер. Таким “документом” на переменную является ее адрес в памяти. Но нам не хочется оперировать самими адресами, нужно что-то более подходящее. Для этого служат указатели(pointers), это переменные, которые хранят адреса переменных.
int x = 0;
int * pX = nullptr;
pX = &x;
int y = *pX;
-
Во-вторых, нам хотелось бы создавать и освобождать память, когда мы того сами захотим, без привязки к пространствам имен или времени жизни программы. Нужны данные - разместили (new), нет в них необходимости – удалили (delete), т.е. полный контроль. Для этих целей нам нужна отдельная область памяти и называется она куча(heap). Это место, где размещаются значения, на которые указывают указатели, в свою очередь сами указатели размещаются в стеке. Работа с указателями всегда считалась не простой, т.к. возлагает ответственность за освобождение памяти на человека (в отличии от автоматических переменных) и может привести к утечкам памяти. Облегчить ситуацию призваны так называемые “умные указатели”, а некоторые современные языки программирования (Java, C#) решают эту задачу с помощью “сборщиков мусора”. На что хотелось бы обратить внимание, при освобождении памяти, на которую указывает указатель, сам указатель также требуется обнулить (nullptr), рассмотрим на примере:

#include <iostream>

class Box{
public:
    Box();
    ~Box();
private:
    int bigData;
};

Box::Box() {
    std::cout << "Memory allocated!\n";
}

Box::~Box() {
    std::cout << "Memory deallocated!\n";
}

int main() {
    std::cout << "Pointer created!\n";
    Box * box = nullptr;
    std::cout << "address: " << box << std::endl;

    box = new Box();
    std::cout << "address: " << box << std::endl;

    delete box;
    std::cout << "address: " << box << std::endl;

    std::cout << "Pointer released!\n";
    box = nullptr;
    std::cout << "address: " << box << std::endl;

    return 0;
}
 
Output:
>>Pointer created!
>>address: 0
>>Memory allocated!
>>address: 0x26e00
>>Memory deallocated!
>>address: 0x26e00
>>Pointer released!
>>address: 0 

Перегуд В.