SDK Multiplataforma en C logo

SDK Multiplataforma en C

Uso de C++

❮ Anterior
Siguiente ❯

Los servidores web están escritos en C y, si no lo están, están escritos en Java o C++, que son derivados de C, o en Python o Ruby, que se implementan en C. Rob Pike


La programación orientada a objetos (encapsulación, herencia y polimorfismo) es una herramienta muy poderosa para modelar cierto tipo de problemas. Sin embargo, en NAppGUI creemos que es incorrecto imponer una jerarquía de clases a nivel de SDK, ya que este es un nivel demasiado bajo. El SDK está más cerca del sistema operativo y de la máquina que de los problemas del mundo real resueltos por las aplicaciones, donde una estrategia orientada a objetos puede ser más acertada (o puede que no).

Aunque NAppGUI ha sido diseñado para crear aplicaciones en C "puro", es posible utilizar C++ o combinar ambos lenguajes. Daremos algunos consejos, portando nuestra aplicación ¡Hola Mundo! a C++ (Figura 1).

Captura del programa ¡Hola, mundo! en C++, versión Windows. Captura del programa ¡Hola, mundo! en C++, versión macOS.
Figura 1: Migración de ¡Hola, mundo! a C++.

1. Encapsulación

NAppGUI no impone ninguna jerarquía de clases, lo que deja al programador la libertad de realizar la encapsulación mediante sus propias clases. Evidentemente, dado que C++ incluye a C, podremos llamar a cualquier función C del SDK dentro de una función miembro. Por ejemplo, podemos encapsular la ventana principal de esta manera.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class MainWindow
{
public:
    MainWindow();
    ~MainWindow();

private:
    static void i_OnClose(MainWindow *window, Event *e);
    static void i_OnButton(MainWindow *window, Event *e);
    Panel *i_panel(void);

    Window *window;
    TextView *text;
    uint32_t clicks;
};

Como puedes ver, con respecto a la versión C, i_panel ya no necesita parámetros, ya que usa el puntero implícito this para acceder a los miembros de la clase.


2. Callbacks de clase

Los manejadores de evento son funciones C cuyo primer parámetro es un puntero al objeto que recibe el mensaje. Esto funciona de la misma manera usando funciones estáticas dentro de una clase de C++:

1
2
3
4
5
...
static void i_OnClose(MainWindow *window, Event *e);
...
window_OnClose(this->window, listener(this, i_OnClose, MainWindow));
...

Sin embargo, es posible que queramos utilizar funciones miembro como manejadores del evento, usando el puntero this como receptor. Para hacer esto, derivamos nuestra MainWindow de la interfaz IListener y usamos la macro listen en lugar de listener().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class MainWindow : public IListener
{
...
    void i_OnClose(Event *e);
    void i_OnButton(Event *e);
...
};

void MainWindow::i_OnButton(Event *e)
{
    String *msg = str_printf("Button click (%d)\n", this->clicks);
    ...
}
...
button_OnClick(button, listen(this, MainWindow, i_OnButton));
...
IListener es una interfaz C++ que permite utilizar métodos miembro de una clase como manejadores de evento.

También es posible dirigir el evento a un objeto diferente (y de diferente clase) a la dueña control. Para ello indicamos el receptor como primer parámetro de listen, como vemos a continuación. La pulsación del botón de cierre será procesada en la clase App y no en MainWindow.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class App : public IListener
{
public:
    App();
    ~App();
    void i_OnClose(Event *e);

private:
    MainWindow *main_window;
};

class MainWindow : public IListener
{
public:
    MainWindow(App *app);
}

MainWindow::MainWindow(App *app)
{
   ...
   window_OnClose(this->window, listen(app, App, i_OnClose));
   ...
}

void App::i_OnClose(Event *e)
{
    osapp_finish();
}
Podemos establecer como receptor de eventos, cualquier objeto que implemente la interfaz IListener.

3. Combinar módulos C y C++

Un proyecto C/C++ selecciona el compilador en función de la extensión del archivo. Para *.c se utilizará el compilador C y para *.cpp el compilador C++. El mismo proyecto puede combinar módulos en ambos lenguajes si consideramos lo siguiente.

3.1. Uso de C desde C++

No hay problema si las declaraciones de funciones la cabecera de C están entre las macros: __EXTERN_C y __END_C.

1
2
3
4
5
6
7
__EXTERN_C

real32_t mymaths_add(const real32_t a, const real32_t b);

real32_t mymaths_sub(const real32_t a, const real32_t b);

__END_C
__EXTERN_C y __END_C son alias para extern "C" {}. Esto le dice al compilador de C++ que no use name mangling con funciones en C.

3.2. Uso de C++ desde C

C no entiende la palabra clave class y dará un error de compilación al incluir cabeceras de C++. Es necesario definir una interfaz en C sobre código C++.

mywindow.h
1
2
3
4
5
6
7
8
9
__EXTERN_C

typedef struct _mywin_t MyWindow;

MyWindow *mywindow_create();

void mywindow_move(MyWindow *window, const real32_t x, const real32_t y);

__END_C
mywindow.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class MainWindow
{
public:
    MainWindow();
    void move(const real32_t x, const real32_t y);
};

MyWindow *mywindow_create()
{
    return (MyWindow*)new MainWindow();
}

void mywindow_move(MyWindow *window, const real32_t x, const real32_t y)
{
    ((MainWindow*)window)->move(x, y);
}

4. Sobrecarga de new y delete

C++ utiliza los operadores new y delete para crear instancias dinámicas de objetos. Podemos hacer que las reservas se realicen a través de Heap, el gestor de Heap - Gestor de memoria que incorpora NAppGUI, con el fin de optimizar C++ y controlar los Memory Leaks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class MainWindow : public IListener
{
    ...
    void *operator new(size_t size)
    {
        return (void*)heap_malloc((uint32_t)size, "MainWindow");
    }

    void operator delete(void *ptr, size_t size)
    {
        heap_free((byte_t**)&ptr, (uint32_t)size, "MainWindow");
    }
    ...
};

5. Hola C++ completo

Listado 1: demo/hellocpp/main.cpp
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/* NAppGUI C++ Hello World */

#include <nappgui.h>

class App;

class MainWindow : public IListener
{
public:
    MainWindow(App *app);
    ~MainWindow();

    void *operator new(size_t size) { return (void*)heap_malloc((uint32_t)size, "MainWindow"); }
    void operator delete(void *ptr, size_t size) { heap_free((byte_t**)&ptr, (uint32_t)size, "MainWindow"); }

private:
    void i_OnButton(Event *e);
    Panel *i_panel(void);

    Window *window;
    TextView *text;
    uint32_t clicks;
};

/*---------------------------------------------------------------------------*/

class App : public IListener
{
public:
    App();
    ~App();
    void i_OnClose(Event *e);
    void *operator new(size_t size) { return (void*)heap_malloc((uint32_t)size, "App"); }
    void operator delete(void *ptr, size_t size) { heap_free((byte_t**)&ptr, (uint32_t)size, "App"); }

private:
    MainWindow *main_window;
};

/*---------------------------------------------------------------------------*/

void MainWindow::i_OnButton(Event *e)
{
    String *msg = str_printf("Button click (%d)\n", this->clicks);
    textview_writef(this->text, tc(msg));
    str_destroy(&msg);
    this->clicks += 1;
    unref(e);
}

/*---------------------------------------------------------------------------*/

Panel *MainWindow::i_panel(void)
{
    Panel *panel = panel_create();
    Layout *layout = layout_create(1, 3);
    Label *label = label_create();
    Button *button = button_push();
    TextView *textv = textview_create();
    this->text = textv;
    label_text(label, "Hello!, I'm a label");
    button_text(button, "Click Me!");
    button_OnClick(button, IListen(this, MainWindow, i_OnButton));
    layout_label(layout, label, 0, 0);
    layout_button(layout, button, 0, 1);
    layout_textview(layout, textv, 0, 2);
    layout_hsize(layout, 0, 250);
    layout_vsize(layout, 2, 100);
    layout_margin(layout, 5);
    layout_vmargin(layout, 0, 5);
    layout_vmargin(layout, 1, 5);
    panel_layout(panel, layout);
    return panel;
}

/*---------------------------------------------------------------------------*/

void App::i_OnClose(Event *e)
{
    osapp_finish();
    unref(e);
}

/*---------------------------------------------------------------------------*/

MainWindow::MainWindow(App *app)
{
    Panel *panel = i_panel();
    this->window = window_create(ekWINDOW_STD);
    this->clicks = 0;
    window_panel(this->window, panel);
    window_title(this->window, "Hello, C++!");
    window_origin(this->window, v2df(500, 200));
    window_OnClose(this->window, IListen(app, App, i_OnClose));
    window_show(this->window);
}

/*---------------------------------------------------------------------------*/

MainWindow::~MainWindow()
{
    window_destroy(&this->window);
}

/*---------------------------------------------------------------------------*/

App::App(void)
{
    this->main_window = new MainWindow(this);
}

/*---------------------------------------------------------------------------*/

App::~App()
{
    delete this->main_window;
}

/*---------------------------------------------------------------------------*/

static App *i_create(void)
{
    return new App();
}

/*---------------------------------------------------------------------------*/

static void i_destroy(App **app)
{
    delete *app;
    *app = NULL;
}

/*---------------------------------------------------------------------------*/

#include "osmain.h"
osmain(i_create, i_destroy, "", App)

6. Plantillas matemáticas

En NAppGUI existen dos versiones para todas las funciones y tipos matemáticos (Listado 2): float (real32_t) y double (real64_t). Podemos utilizar unos u otros según convenga en cada caso.

Listado 2: Cabecera bmath.h (parcial).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/* Math funcions */

#include "osbs.hxx"

__EXTERN_C

real32_t bmath_cosf(const real32_t angle);

real64_t bmath_cosd(const real64_t angle);

real32_t bmath_sinf(const real32_t angle);

real64_t bmath_sind(const real64_t angle);

extern const real32_t kBMATH_PIf;
extern const real64_t kBMATH_PId;
extern const real32_t kBMATH_SQRT2f;
extern const real64_t kBMATH_SQRT2d;

__END_C
Todas las funciones y tipos en simple precisión acaban con el sufijo "f" y las de doble precisión en "d".

Cuando implementamos funciones geométricas o algebraicas más complejas, no es fácil tener claro de antemano cual es la precisión correcta. Ante la duda siempre podemos optar por utilizar double, pero esto tendrá un impacto en el rendimiento, sobre todo por el uso del ancho de banda de memoria. Pensemos en el caso de mallas 3D con miles de vértices. Sería estupendo disponer de ambas versiones y poder utilizar una u otra según cada caso concreto.

Por desgracia el lenguaje C "puro" no permite programar con tipos genéricos, al margen de utilizar horribles e interminables macros. Deberemos implementar ambas versiones (float y double), con el coste de mantenimiento asociado. C++ resuelve el problema gracias a las plantillas (template<>). La contrapartida es que, normalmente, debemos "abrir" la implementación e incluirla en la cabecera .h, ya que el compilador no sabe generar el código máquina hasta que la plantilla se instancia con un tipo de dato concreto. Esto choca de frente con nuestros Estándares, sobre todo en la parte relativa a la encapsulación de información. A continuación veremos como utilizar las templates de C++ para obtener lo mejor de ambos casos: Programación genérica, ocultar implementaciones y mantener las cabeceras "limpias".

Al igual que existe una cabecera *.h para cada módulo matemático, existe una contrapartida *.hpp utilizable únicamente desde módulos C++ (Listado 3).

Listado 3: Cabecera bmath.hpp (parcial).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/* Math funcions */

#include "osbs.hxx"

template<typename real>
struct BMath
{
    static real(*cos)(const real angle);

    static real(*sin)(const real angle);

    static const real kPI;
    static const real kSQRT2;
};

Estas plantillas contienen punteros a función, cuyas implementaciónes están ocultas en bmath.cpp. En (Listado 4) tenemos un ejemplo de uso.

Listado 4: Implementación de un algoritmo genérico.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include "bmath.hpp"

template<typename real>
static void i_circle(const real r, const uint32_t n, V2D<real> *v)
{
    real a = 0, s = (2 * BMath<real>::kPI) / (real)n;
    for (uint32_t i = 0; i < n; ++i, a += s)
    {
        v[i].x = r * BMath<real>::cos(a);
        v[i].y = r * BMath<real>::sin(a);
    }
}

Este algoritmo está implementado dentro de un módulo C++ (Listado 5), pero queremos poder llamarlo desde otros módulos, tanto C como C++. Para ello definiremos los dos tipos de cabecera: *.h (Listado 6) y *.hpp (Listado 7).

Listado 5: mymath.cpp. Implementación.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include "mymath.h"
#include "mymath.hpp"
#include "bmath.hpp"

template<typename real>
static void i_circle(const real r, const uint32_t n, V2D<real> *v)
{
    real a = 0, s = (2 * BMath<real>::kPI) / (real)n;
    for (uint32_t i = 0; i < n; ++i, a += s)
    {
        v[i].x = r * BMath<real>::cos(a);
        v[i].y = r * BMath<real>::sin(a);
    }
}

void mymath_circlef(const real32_t r, const uint32_t n, V2Df *v)
{
    i_circle<real32_t>(r, n, (V2D<real32_t>*)v);
}

void mymath_circled(const real64_t r, const uint64_t n, V2Dd *v)
{
    i_circle<real64_t>(r, n, (V2D<real64_t>*)v);
}

template<>
void(*MyMath<real32_t>::circle)(const real32_t, const uint32_t, V2D<real32_t>*) = i_circle<real32_t>;

template<>
void(*MyMath<real64_t>::circle)(const real64_t, const uint32_t, V2D<real64_t>*) = i_circle<real64_t>;
Listado 6: mymath.h. Cabecera C.
1
2
3
4
5
6
7
8
9
#include "geom2d.hxx"

__EXTERN_C

void mymath_circlef(const real32_t r, const uint32_t n, V2Df *v);

void mymath_circled(const real64_t r, const uint64_t n, V2Dd *v);

__END_C
Listado 7: mymath.hpp. Cabecera C++.
1
2
3
4
5
6
7
#include "v2d.hpp"

template<typename real>
struct MyMath
{
    void (*circle)(const real r, const uint32_t n, V2D<real> *v);
};

Ahora podemos utilizar nuestra librería matemática en C y C++, tanto en float como en double precisión (Listado 8).

Listado 8: Uso de mymaths en algoritmos genéricos C++.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include "mymath.hpp"
#include "t2d.hpp"

template<typename real>
static void i_ellipse(const real r1, const real r2, const uint32_t n, V2D<real> *v)
{
    T2D<real> transform;
    T2D<real>::scale(&transform, r1, r2);

    MyMath<real>::circle(1, n, v);

    for (uint32_t i = 0; i < n; ++i)
        T2D<real>::vmult(&transform, &v[i]);
}
❮ Anterior
Siguiente ❯