Wednesday, September 15, 2021

CLion + MinGW + Raylib + {fmt}

1. MinGW-w64

WINLIBS

1.1 Unzip archive to C:\mingw64 folder.

1.2 Add system environment variables:

PATH
C:\mingw64\bin

C_INCLUDEDE_PATH
C:\mingw64\include\

CPLUS_INCLUDE_PATH
C:\mingw64\include\; C:\mingw64\include\c++\11.2.0

LIBRARY_PATH
C:\mingw64\lib    

1.3 Check GCC version: gcc -v

1.4 Configure CLion Toolchains Settings

 

2. {fmt}

WWW

GITHUB

2.1 Download and unzip Source zipfile.

2.2 Open and compile in CLion as cmake project to static library.

2.3 Copy "libfmt.a" to C:\mingw64\lib

2.4 Copy "fmt" folder with headers to C:\mingw64\include


3. Raylib

WWW

GITHUB

3.1 Download and unpack "raylib-3.7.0_win64_mingw-w64.zip"

3.2  Copy static and dynamic libs "libraylib.a, libraylibdll.a" to C:\mingw64\lib for linking

3.3 Copy raylib.dll to C:\mingw64\bin for running

3.4 Copy header "raylib.h" to C:\mingw64\include

 

4. Make C++ project in CLion

4.1 Edit "CMakeLists.txt"

cmake_minimum_required(VERSION 3.20)
project(<target>)

set(CMAKE_CXX_STANDARD 20)

add_executable(<target> main.cpp)

target_link_libraries(<target> fmt raylibdll)

4.2 Edit "main.cpp"

#include <raylib.h>
#include <iostream>
#include <fmt/core.h>

int main(void) {

    InitWindow(800, 450, "raylib [core] example - basic window");

    while (!WindowShouldClose()) {
        BeginDrawing();
        ClearBackground(RAYWHITE);
        DrawText("Congrats! You created your first window!", 190, 200, 20, LIGHTGRAY);
        EndDrawing();
    }

    CloseWindow();

    int x = 5;
    std::cout << fmt::format("x = {}", x);

    return 0;
}

 

 

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

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