Будущее C: ООП

Интернет обошла стороной одна интересная новость: стало известно о том, что, вероятно, в следующий стандарт языка программирования C будут добавлены средства ООП, а именно — классы. При этом судить о том, будет ли это реализовано — ещё рано, так как данный документ не был окончательно принят и не добавлялся в черновой вариант следующего стандарта. Предложение реализовать это поступило ещё в далёком 1995 году неким Robert Jervis, но было принято на WG14 только сейчас.

Попробуем поделить шкуру неубитого медведя и посмотрим, чем это грозит и что это нам даст.

Для этого рассмотрим пару областей данного средства:

GUI


Многим известно, что в этом деле без ООП практически невозможно что-либо сделать по уму. Так, в GTK+ используется имитация ООП посредством GObject.

Hello, world!
#include <gtk/gtk.h>
 
int main (int argc, char *argv[])
{
    GtkWidget *window;
    GtkWidget *label;
 
    gtk_init(&argc, &argv);
 
    /* Create the main, top level window */
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
 
    /* Give it the title */
    gtk_window_set_title(GTK_WINDOW(window), "Hello, world!");
 
    /* Center the window */
    gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
 
    /* Set the window's default size */
    gtk_window_set_default_size(GTK_WINDOW(window), 200, 100);
 
    /*
    ** Map the destroy signal of the window to gtk_main_quit;
    ** When the window is about to be destroyed, we get a notification and
    ** stop the main GTK+ loop by returning 0
    */
    g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
 
    /*
    ** Assign the variable "label" to a new GTK label,
    ** with the text "Hello, world!"
    */
    label = gtk_label_new("Hello, world!");
 
    /* Plot the label onto the main window */
    gtk_container_add(GTK_CONTAINER(window), label);
 
    /* Make sure that everything, window and label, are visible */
    gtk_widget_show_all(window);
 
    /*
    ** Start the main loop, and do nothing (block) until
    ** the application is closed
    */
    gtk_main();
 
    return 0;
}


Мы наблюдаем классическую для C картину — управление данными в стеке через указатели. Это требует дополнительного внимания к объявлению этих самых указателей. Также это заметно отразится на сопровождению кода.

Вместо этого можно было бы использовать “нативный” подход к разработке, как это реализовано в GTKMM (C++).

Hello, world!
//HelloWorldWindow.h
#ifndef HELLOWORLDWINDOW_H
#define HELLOWORLDWINDOW_H
 
#include <gtkmm/window.h>
#include <gtkmm/button.h>
 
// Derive a new window widget from an existing one.
// This window will only contain a button labelled "Hello World"
class HelloWorldWindow : public Gtk::Window
{
  public:
    HelloWorldWindow();
 
  protected:
    void on_button_clicked(); //event handler
 
    Gtk::Button hello_world;
};
 
#endif

//HelloWorldWindow.cc
#include <iostream>
#include "HelloWorldWindow.h"
 
HelloWorldWindow::HelloWorldWindow()
 : hello_world("Hello World")
{
    // Set the title of the window.
    set_title("Hello World");
 
    // Add the member button to the window,
    add(hello_world);
 
    // Handle the 'click' event.
    hello_world.signal_clicked().connect(
        sigc::mem_fun(*this, &HelloWorldWindow::on_button_clicked));
 
    // Display all the child widgets of the window.
    show_all_children();
}
 
void HelloWorldWindow::on_button_clicked()
{
    std::cout << "Hello world" << std::endl;
}

//main.cc
 
#include <gtkmm/main.h>
#include "HelloWorldWindow.h"
 
int main(int argc, char *argv[]) 
{
    // Initialization
    Gtk::Main kit(argc, argv);
 
    // Create a hello world window object
    HelloWorldWindow example;
 
    // gtkmm main loop
    Gtk::Main::run(example);
    return 0;
}


Это, бесспорно, выглядит более многословно, но исчезают проблемы, присущие указателям, такие как утечки памяти, ошибки разыменования, удаление NULL-указателей. Также мы сразу же получаем преимущества ООП, такие как инкапсуляция, полиморфизм и наследование. Это явно находит применение в данной области, особенно наследование.

Низкоуровневое программирование


Одна из основных ниш, которую занимает этот пресловутый язык. Решить подобную проблему на C можно разными способами, но наиболее часто встречается непосредственное обращение к регистрам машины.

PORTD = 0xff;

Но это содержит очевидные минусы: лёгкость совершить опечатку, генерирует повторяющийся код, неудобочитаемость и сложность сопровождения.

Более удобно было бы реализовать простенький класс.

class led {
public:
	led: port {*s}{/*..*/}
	ledOn(number) {/*...*/};
//...	
};

И в дальнейшем создавать объект для каждого порта. Также можно добавить в конструктор дополнительные параметры, определяющие настройку портов, не говоря уже о огромном множестве всевозможных методов для сложных манипуляций, к примеру, всех разрядов порта.

Проблемный вопрос


В любом случае, области применения данного средства очевидны. Они расширяют функционал языка, делая его не только эффективным, но и более удобным. Из объективной точки зрения это вполне нужно, но… Думаю, у многих ещё при чтении заголовка возник внутренний диссонанс, ведь для этого есть C++. Но разве это ограничит сам C? Кому это должно навредить? Язык уже не так и молод, успел создать массу устойчивых стереотипов, но время идёт, пришло время меняться, и принимать необходимость как должное.
Поделиться публикацией

Комментарии 17

    +7
    Шел 2022 год, поступило предложение добавить в С множественное виртуальное наследование, LINQ и лямбды.
      +2
      так рано? из первоочередного — встроенный «прибитый» сборщик мусора, jit компиляция и безтиповый питон
      0
      похоже на маркетинговый ход: использование раскрученного бренда известного языка там, где должен по идее появиться новый
        +5
        Я ничего не понял.
        Если хотите полноценный ООП используйте C++, не хотите или у вас нет компилятора C++ для целевой платформы, тогда используйте C.
        А статья о чем?
          +3
          первое апреля в этом году на две недели раньше :)
          +1
          Обычно выбирают ЯП для написания проекта, а это выглядит как попытка переделать язык программирования, чтобы он удовлетворял каким-то требованиям и не пришлось учить другой ЯП.
            +2
            в следующий стандарт языка программирования C будут добавлены средства ООП, а именно — классы

            Первое предложение добавить классы в C было еще в 93-м году — N298. Затем в 95-ом — N424, N445-447. Если вы внимательно следите за предложениями для WG14 и WG21, то могли бы заметить, что далеко не всякое предложение попадает в стандарт, а некоторые хотелось бы и вовсе «развидеть».
              –1
              Так есть же уже с++, зачем, лучше бы его доума довели, как-то писал дерево для GUI с наследованиями и т.п. так вот забросил на таком моменте.

              class Object {
              protected:

              Object* owner = nullptr;

              };

              class Node: public Object {
              public:

              void attachChild(Object* child) {
              owner = child;
              // child->owner = this;
              }

              };

              int main() {
              return 0;
              }

              если снять комментарий с:

              // child->owner = this;

              то ошибку будут кидать и gnu и ms компиляторы.

              Хотя возможно можно было бы внедрить что-то вроде наследования для структур (хотя не знаю может уже и есть) и доступ к предкам через какие ни будь (имя структуры предка)a->foo(), хотя если честно не для того ягодка росла чтобы ее ооп портили =)
                0
                Инкапсулируйте тщательнее:

                class Object 
                {
                public:
                    void setOwner(Object * newOwner)
                    {
                        owner = newOwner;
                    }
                
                protected:
                    Object* owner = nullptr;
                };
                
                class Node: public Object 
                {
                public:
                    void attachChild(Object* child) 
                    {
                        owner = child;
                        child->setOwner(this);
                    }
                
                };
                
                
                  0
                  Все верно, но тогда смысл описывать owner как protected =) В общем после всех этих заморочек я искренне полюбил JavaScript =)
                    0
                    по-моему, тут циклическая ссылка
                    мне кажется, с вашей любовью вы понаделаете утечек
                      –1
                      owner = this мной написан чтобы показать, что доступ к protected переменным и методам предка есть, но к сожалению он не распространяется на указатели, это просто пример, задача же заключалась в том, чтобы указать детке кто его предок но доступ к полю был только из объекта, то есть (protected). Конечно можно было бы наплодить гетеров и сетеров (как предлагалось выше) или указать public вместо private, но тогда возникает вопрос, зачем тогда эти public, protected и private надо? Там еще много было заморочек, например почти все мотоды приходилось делать virtual (напомню писалось gui) так как наследуемые элементы интерфейса могли вести себя по разному в результате мы приходил к модели класса имеющего виртуальную таблицу методов, что схоже с кодом ооп языков вроде java или javascript и лишает нас преимуществ c++. Но тут надо попробовать самому чтобы понять, на словах выглядит как полная дурь и что я несу на высокосортной и божественный с++, незря же пост минусуют =)
                        0
                        доступ к protected переменным и методам предка есть, но к сожалению он не распространяется на указатели


                        Он не «не распространяется на указатели», он работает только для экземпляров того класса, которому принадлежит функция — то есть, функция класса Node имеет доступ к protected переменным только экземпляров класса Node:

                        class Base
                        {
                        protected:
                            int i;
                        }
                        
                        class Derived : public Base
                        {
                        public:
                            void foo(int value)
                            {
                                i = value;//Ok, изменяем самого себя
                            }
                            void bar(Derived * other)
                            {
                                 other->i = 0;//Ok, изменяем переменную в другом экземпляре этого же класса
                        }
                        


                        Однако в вашу функцию приходит указатель на базовый класс. По факту этот указатель может указывать на экземпляр любого класса, унаследованного от Object, не обязательно класса Node. Естественно, в этом случае доступ вы не получите. Сделайте, например,
                        static_cast<Node*>(child)->owner = this
                        
                        и все у вас будет работать. Перепишите функцию attachChild() так, чтобы она принимала Node*, и все будет работать. То есть, в вашем конкретном случае, разграничение доступа делает именно то, что должно — разграничивает доступ.
                        Там еще много было заморочек, например почти все мотоды приходилось делать virtual (напомню писалось gui) так как наследуемые элементы интерфейса могли вести себя по разному в результате мы приходил к модели класса имеющего виртуальную таблицу методов, что схоже с кодом ооп языков вроде java или javascript и лишает нас преимуществ c++.
                        Простите, но тут я вас вообще не понимаю. Естественно, если вы хотите иметь полиморфизм, то вам нужны виртуальные функции, оно просто так работает. Каких таких преимуществ C++ нас лишает наличие в классе vtable, мне не очень понятно.
                          –1
                          Он не «не распространяется на указатели», он работает только для экземпляров того класса, которому принадлежит функция — то есть, функция класса Node имеет доступ к protected переменным только экземпляров класса Node:

                          Простите, для меня это равносильно фразе — круглое не квадратное.

                          Правильно или нет (про доступ к защищенным предкам) вопрос личного мнения, и все что я могу тут поделать, это порадоваться, что вы на стороне разработчиков, хотя приведенная вами заплатка с использованием static_cast скорее указывает на то, что их правота не совсем идеальна.

                          Про vtable — если коротко, скорость выполнения кода. Обращение к таблице методов и длинные переходы всегда были медленнее чем вызов функций на прямую или inline методов. Виртуальные методы могут вызываться только использованием long jump переходов из за чего страдают в скорости выполнения (не особо но на критических моментах выполнения программы это может быть заметно). Отчасти из за того что все методы виртуальные такие языки программирования как javascript, java, c# и т.п. уступают в производительности программа на с и с++.
                          Также стоит отметить размер класса который увеличивается на размер этой самой vtable.
                  0
                  Или сделайте внешнюю дружественную функцию вроде addChild(Node* child, Node* parent).

                  А приватные члены можно изменять только в этом экземпляре класса. Это логично, на то они и приватные. Иначе экземпляр класса человек (я) мог бы поменять член деньги у другого экземпляра (Вас).
                  +3
                  Не так быстро.

                  6.7 Classes to C [N1875](Abramson)

                  The straw polls above indicate there is no interest by the Committee to persue
                  further development of the proposal presented.


                  Это из www.open-std.org/jtc1/sc22/wg14/www/docs/n1884.pdf
                  • НЛО прилетело и опубликовало эту надпись здесь
                      0
                      Интересная новость… предвижу, как взвоют адепты олдскульной сишечки:)
                      На самом деле действительно есть С++ (в том числе и для низкоуровневого кодинга, для микроконтроллеров и т.п.)
                      Но если уж вводить — можно еще вместо наследования взять «embedding» из Go — для низкоуровневого языка самое то. Можно включать одну структуру в произвольную позицию другой, при этом позицию программист определяет явно (в отличие от наследования). А какие хаки можно будет на этом делать…
                        0
                        Взвоют, не взвоют, пока даже C99 полностью не поддерживает большинство компиляторов, да и C89 далеко не на всех платформах имеет полную реализацию…
                        0
                        Я вот зашел в основном посмотреть на то, как эти классы должны будут выглядеть. Я не специалист в С, но увидеть такое хотел бы )
                          0
                          А мне так не хватает стабильности… Ну зачем? Зачем выдумывать ненужное? ООП в C это удар под дых… Почему невозможно создать язык и не изменять его никогда больше? Никто же не мешает писать на C++
                            +1
                            PORTD = 0xff;

                            Но это содержит очевидные минусы: лёгкость совершить опечатку, генерирует повторяющийся код, неудобочитаемость и сложность сопровождения.


                            Все зависит от опыта. Опытный Си-разработчик напишет так, что все «очевидные минусы» будут отсутствовать. А начинающий — и «простенький класс» реализует с достаточным количеством минусов.
                            Язык то тут причем? Тут надо что бы руки не под циркуль были заточены (это я конечно грубо, но суть такова).

                            Каждый инструмент имеет свое назначение.

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое