Friday 27 January 2023

A proposal to improve C++ safety

A serious proposal to improve C++ safety

The C++ of old times

When I began my voyage in the land of programming, my teacher taught my classmates and me C++. It was a really good way to start programming, since it gave me a deep undestanding of how a program is written and how computers work (thank you, Renato Conte!).
C++ at the end of the '90s was almost a C with classes. Sure, we had streams, but it lacked lists, vectors and even strings where still C's characters arrays. Without Boost libraries, programming in C++ gave little more than plain C.
In the same period, Java came out, provviding garbage collection, hiding pointers and including a massive standard library: list, vectors, strings were obviously included; sockets, multithreadings with semaphores and Monitors and a portable GUI library (the old old old AWT) were fantastic add-ons.
Sure: Java had a lot of annoying aspects, such as its verbosity (e.g. System.out.println("Hello world!"), a slow compiling time and some annoying perks, such as the lack of unsigned. But its standard library and the fact I had to ignore to manage the memory, looked to me more attractive than C++ when I had to make UI or a network program.
In the early 2000 I wrote my motto: "Python when I can; C when I must; C++ if they torture me".

How stuff changed with C++11

Then came C++11 and it was a smash: while the language still didn't provvide native support to networking, to GUI or to graphics, it came with some real improvements:

  1. A standard library with all the data containers required for a healty life (and strings!)
  2. Multithreading defined as a standard set of objects and functions
  3. The smart pointers, the point of this post

Smart pointers are a real game-change, because they fix one the C++ biggest holes: memory management. They relies on the RAII concept, present already in the '90s C++, but let explain it better.
RAII stands for Resource Acquisition Is Intialization: a overcomplicated way to say

A objects must allocates all the needed resources (memory, connections, file descriptors, etc.) in its constructor; it must release all when it exits its scope, calling the destructor

...
{
    std::vector<int> test;
    for (int i=0: i < 10000; i++)
        test.add(i + 2);
}

Vector test is internally big enough to store ten thousands integers. We can assume these elements will be stored in the heap and not in the stack. In any case, when the program reach the end of the block, because of RAII the compiler added the instructions needed to call std::vector 's destructor and all its resources will be released.
Having RAII do we still need pointers? Well, yes: because we need sometimes to have objects able to exist outside the block where they are declared. Problem is pointers are an old construct of C and they do not support RAII.
Smart pointers are objects able to store a pointer to another object. And since they are objects, when they exit their scope they call their destructor… and the destructor of the object they contains.
E.g.:

std::unique_ptr<Window> make_basic_window()
{
  auto window = std::make_unique<Window>(800, 600, std::string("Program window"));
  window->add_button(std::make_unique<Button>("Button one"));
  window->add_button(std::make_unique<Button>("Button two"));
  window->add_button(std::make_unique<Button>("Button three"));

  return window;
}

The Window object will survive to the end of make_basic_window function, returning a std::unique_ptr which will:

  1. Die at the end of the calling block
  2. Survive if its ownership will be passed to another smart pointer through a call to std::move.

Here two examples

{
  std::unique_ptr window = make_basic_window();
  window->set_title("A new title");
  window->add_button(std::make_unique<Button>("Button four");
}
// where window object dies because it exits the scope where was declared
void modify_window(std::unique_ptr<Window>& result)
{
  std::unique_ptr window = make_basic_window();
  window->set_title("Survivor window");

  result = std::move(window);

  // we MOVE the result to another pointer, so the object will survive its scope
}

Unsafe blocks for old C parts

Many complained C++ was, at the beginning, just replicating functionalities already present in C. Ad example, std::cout was a replacement of printf and ifstream with ofstream a substitute to fopen.
But with C++11 standard, we reached the point this language became indipendent from C. Sure, compatibility with C is still a plus: what I mean is that all unsafe aspect of the illustrious predecessor have a safer correspondance.
For this reason, as Rust does, we should introduce in C++ the unsafe block: this way, everytime we use features NOT SAFE, the compiler should warn us if they're not closed in a block the programmer knows could create problems. Things such as structs, new, delete and pointers in general:

std::unique_ptr<GtkWidget> safe_new_gtk_window(std::unique_ptr<GtkApplication> app)
{
  std::unique_ptr<GtkWidget> safe_window;
  unsafe
  {
    GtkWidget *window;

    window = gtk_application_window_new (app);
    gtk_window_set_title (GTK_WINDOW (window), "Window");
    gtk_window_set_default_size (GTK_WINDOW (window), 200, 200);
    gtk_widget_show (window);

    safe_window = std::unique_ptr<GtkWidget>(window);
  }
  return safe_window;
}

In this piece of code we have pointers, functions and structs, but they're all enclosed in a unsafe block. This does not make manage the resources: it's just a placeholder to tell the compiler: «I KNOW this part could be potentially dangerous. I managed it at it best, so do not interrupt compilation».
Because this should do this new unsafe block: the old C elements are not removed, just trigger the compiler the situation could be source of problems.

C++ is not anymore the language invented in the 90s, generations of people learnt this difficult languages when it was unsafe and still cannot decide if using a struct or a class in certain situations.
So, spread the word!

We need the UNSAFE block in C++ for all the old, dangerous C constructs

Created: 2023-01-26 Thu 10:29

Validate

2 comments:

Anonymous said...

In the last code example, itbcould bestehst the line
safe_window = gtk_application_window_new (app);
should read instead?
window = gtk_application_window_new (app);

Just an idea.
Martin Raabe

JaK said...

Thank you Martin! You're right! :-) I fixed it.