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 

Перегуд В.

Thursday, October 31, 2019

Полиморфизм в си++.


Что такое полиморфизм? Давайте разбираться вместе. В каком-то смысле это свойство пользовательских типов. В более современных языках, таких как Java, в которых нет ограничений по обратной совместимости, полиморфизм “включен” по умолчанию. В нашем случае это не так, и его нужно включать принудительно. Но погодите, сам по себе полиморфизм не существует, сначала нам понадобится наследование. Вообще пользовательские типы это много всякой "дури", но не будем хвататься за все сразу. Наследовать свойства одних типов другими это удобно, преимущества такого подхода всем известны, это дает нам возможность строить родительские связи между типами и работать с наследниками как потомками одного родительского типа.

Давайте рассмотрим пример. Мы имеем родительский класс животные и его наследников кот и собака.
class Cat : public Animal {}; Cat cat;
Сложим наших наследников в контейнер и обработаем каждый элемент в цикле, например, вызовем унаследованный от родительского типа метод – подать сигнал. Создав контейнер, который содержит "элементы" родительского типа - животные и так как этому условию соответствуют наши наследники – кот и собака, помещаем их туда.
vector<Animal*> army = {&cat, &dog};
for(auto solder : army){ solder->signal(); }
 
Далее мы можем "решать проблему" тремя способами.
 
Первый способ – мы хотим сохранить нашу систему как можно более простой и принимаем как должное, что все животные могут “говорить”, просто издавать звуки, это достаточный для нас уровень абстракции, и мы не будем его дальше детализировать. Потому что на уровне наследников нам не важно, что именно они “говорят”, а более важно к примеру, что кот ловит мышей, а собака нарушителей границы, но при этом они могут подать звуковой сигнал. Создадим в родителе общий для всех наследников метод один раз и можем его использовать в любом наследнике, которых может быть любое количество.
class Animal{ void signal() { todo(); } }; 
 
Второй способ, когда у нас возникла необходимость конкретизировать поведение некоторых наследников. По каким-то причинам это может понадобится - переопределить поведение наследника, чтобы оно отличалось от родительского.
class Cat : public Animal{ void signal() { todo(); } };
Но в отличии от Java, только этого недостаточно. Мы должны указать в родительском типе, что при наличии преопределенного метода в наследнике, нужно использовать именно его, с помощью ключевого слова virtual:
virtual void signal() { todo(); }
В добавок для удобства можно использовать ключевое слово override в наследнике:
void signal() override { todo(); }
 
Третий способ, это частный случай второго способа, когда у нас возникла необходимость конкретизировать поведение всех наследников. В этом случае, с одной стороны нам не нужно определять общий для вех наследников метод - мы это сделаем в самих наследниках, с другой стороны нам нужно как-то указать на необходимость обязательного определения такого поведения в наследниках, т.к. если мы его не определим в родителе или наследнике, то при его вызове получим ошибку. В этом случае нам поможет чисто виртуальный метод, записанный в родительском типе в таком виде:
virtual void signal() = 0;
 
“И где тут полиморфизм?”, спросите вы. И вообще зачем он? Для меня ответ такой – это инструмент, который позволяет решать задачу действуя от общего к частному, что часто очень полезно. При таком подходе мы можем создать скелет сложной системы и потом изменять ее, путем добавления или замены ее элементов, которые будут вести себя по разному, настоящая магия и превращения (morph). В нашем случае мы работаем с животными, которые могут быть и вести себя как кот, собака и т.д. Давайте рассмотрим сам механизм:
Animal* x = &cat;
x->signal();
Слева у нас родительский тип животное, справа наследник кот. И у родителя, и у наследника есть поведение - сигнал, какой его вариант будет использован при вызове, родительский или потомка?
 
Если мы хотим, чтобы использовался родительский, это обычное наследование без полиморфизма, это рассмотренный выше первый способ. Если мы хотим использовать вариант потомка, что в свою очередь дает возможность использовать собственные варианты для разных потомков, то есть вести себя по-разному – полиморфно. То определяем родительское поведение, как виртуальное, это рассмотренный выше второй способ.
 
Код, который можно скомпилировать:
#include <iostream>
#include <vector>

class Animal {
public:
    virtual void signal() {
        std::cout << "Animal!\n";
    }
};

class Cat : public Animal {
public:
    void signal() override {
        std::cout << "Cat!\n";
    }
};

class Dog : public Animal {
public:
    //void signal() override {
    //    std::cout << "Dog!\n";
    //}
};

int main() {
    Cat cat;
    Dog dog;
    std::vector<Animal *> army = {&cat, &dog};
    for (auto solder:army) {
        solder->signal();
    }

    return 0;
}

Output:
>>Cat!
>>Animal!

P.S. Статья писалась с целью навести порядок в голове, буду рад если еще кому-то будет полезно.
Перегуд В.