Cross-platform C SDK logo

Cross-platform C SDK

Use of C++

❮ Back
Next ❯
This page has been automatically translated using the Google Translate API services. We are working on improving texts. Thank you for your understanding and patience.

Web servers are written in C, and if they're not, they're written in Java or C++, which are C derivatives, or Python or Ruby, which are implemented in C. Rob Pike


Object-oriented programming (encapsulation, inheritance and polymorphism) is a very powerful tool for modeling certain kinds of problems. However, at NAppGUI we believe that it is wrong to impose a class hierarchy at the SDK level, as this is too low a level. The SDK is closer to the operating system and the machine than to the real-world problems solved by applications, where an object-oriented approach may (or may not) be more successful.

Although NAppGUI has been designed to create applications in "pure" C, it is possible to use C++ or mix both languages. We'll give some advice, porting our Hello World! application to C++ (Figure 1).

Screenshot of the program Hello, world! in C++, Windows version. Screenshot of the program Hello, world! in C++, macOS version.
Figure 1: Migration from Hello, world! to C++.

1. Encapsulation

NAppGUI does not enforce any class hierarchy, leaving the programmer the freedom to encapsulate using their own classes. Of course, since C++ includes C, we can call any SDK C function inside a member function. For example, we can encapsulate the main window like this.

 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;
};

As you can see, relative to the C version, i_panel no longer needs parameters, as it uses the implicit this pointer to access class members.


2. Class callbacks

Event handlers are C functions whose first parameter is a pointer to the object that receives the message. This works the same way using static functions within a C++ class:

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

However, we may want to use member functions as event handlers, using the this pointer as the receiver. To do this, we derive our MainWindow from the IListener interface and use the listen macro instead of 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 is a C++ interface that allows you to use class member methods as event handlers.

It is also possible to direct the event to a different object (and of a different class) than the control owner. To do this, we indicate the receiver as the first parameter of listen, as we see below. The click of the close button will be processed in the App class and not in 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();
}
We can establish as event receiver, any object that implements the IListener interface.

3. Combine C and C++ modules

A C/C++ project selects the compiler based on the file extension. For *.c the C compiler will be used and for *.cpp the C++ compiler. The same project can combine modules in both languages ​​if we consider the following.

3.1. Using C from C++

There is no problem if the C header function declarations are between the macros: __EXTERN_C and __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 and __END_C are aliases for extern "C" {}. This tells the C++ compiler not to use name mangling with C functions.

3.2. Using C++ from C

C does not understand the class keyword and will give a compile error when including C++ headers. It is necessary to define an interface in C over C++ code.

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. new and delete overload

C++ uses the new and delete operators to create dynamic instances of objects. We can make reservations through Heap, the Heap manager that NAppGUI incorporates, in order to optimize C++ and control 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. Hello C++ complete

Listing 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. Math templates

In NAppGUI there are two versions for all (Listing 2) functions and math types: float (real32_t) and double (real64_t). We can use one or the other as appropriate in each case.

Listing 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
All single-precision functions and types end with the suffix "f" and double-precision types end with "d".

When we implement more complex geometric or algebraic functions, it is not easy to be clear in advance what the correct precision is. When in doubt, we can always choose to use double, but this will have an impact on performance, especially due to the use of memory bandwidth. Consider the case of 3D meshes with thousands of vertices. It would be great to have both versions and be able to use one or the other according to each specific case.

Unfortunately the "pure" C language does not allow programming with generic types, apart from using horrible and endless macros. We will have to implement both versions (float and double), with the associated maintenance cost. C++ solves the problem thanks to templates (template<>). The downside is that, normally, we must "open" the implementation and include it in the .h header, since the compiler does not know how to generate the machine code until the template is instantiated with a specific data type. . This is in direct conflict with our Standards, especially in the part related to information encapsulation. Next we will see how to use C++ templates to get the best of both cases: Generic programming, hiding implementations and keeping headers "clean".

Just as there is a *.h header for every math module, there is a counterpart *.hpp usable only from C++ (Listing 3) modules.

Listing 3: Header bmath.hpp (partial).
 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;
};

These templates contain pointers to functions, whose implementations are hidden in bmath.cpp. In (Listing 4) we have an example of use.

Listing 4: Implementation of a generic algorithm.
 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);
    }
}

This algorithm is implemented within a C++ module (Listing 5), but we want to be able to call it from other modules, both C and C++. To do this we will define the two types of headers: *.h (Listing 6) and *.hpp (Listing 7).

Listing 5: mymath.cpp. Implementation.
 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>;
Listing 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
Listing 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);
};

Now we can use our math library in C and C++, both in float and double precision (Listing 8).

Listing 8: Using mymaths in generic C++ algorithms.
 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]);
}
❮ Back
Next ❯