Реализация RAII в чистом C?

Возможно ли реализовать RAII в чистом C?

Я предполагаю, что это невозможно каким-либо вменяемым способом, но, возможно, возможно ли это с помощью какого-то подвоха. В freeголову приходит перегрузка стандартной функции или, возможно, перезапись адреса возврата в стеке, чтобы при возврате функции вызывать какую-то другую функцию, которая каким-то образом освобождает ресурсы? Или, может быть, с какой-то хитростью setjmp / longjmp?

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

15.12.2008 13:40:09
Вы не можете просто перезаписать адрес возврата в стеке; Вы должны сохранить значение при вводе, а затем перезаписать его альтернативой. Уродливо, но, возможно, эффективно. Рассмотрите возможность использования памяти на основе арены. В противном случае просто будьте очень осторожны (и беспокойтесь о прерываниях!).
Jonathan Leffler 15.12.2008 15:37:19
Полезно ли RAII в отсутствие исключений? (просто спрашиваю)
Josh Petitt 7.06.2013 21:49:45
@JoshPetitt конечно, раннее возвращение, и просто не нужно помнить, чтобы освободить каждую вещь = меньше ошибок.
Oktalist 7.06.2013 22:14:41
@JoshPetitt ты, по крайней мере, должен написать еще одно заявление. например, fopen без соответствующего fclose
Justin Meiners 11.07.2013 02:13:38
Я удивлен, что никто не предложил вам использовать компилятор C ++ и писать на этом тайном диалекте C, который компилируется C ++ (просто используя функции RAII, когда вы этого хотите). Я также удивлен, что вы не приняли ответ Йоханнеса, если только вы не настаиваете на «более общем» решении.
jxh 17.02.2017 18:13:39
11 ОТВЕТОВ
РЕШЕНИЕ

Это зависит от реализации, поскольку в стандарте такой возможности нет. Для GCC cleanupатрибут запускает функцию, когда переменная выходит из области видимости:

#include <stdio.h>

void scoped(int * pvariable) {
    printf("variable (%d) goes out of scope\n", *pvariable);
}

int main(void) {
    printf("before scope\n");
    {
        int watched __attribute__((cleanup (scoped)));
        watched = 42;
    }
    printf("after scope\n");
}

Печать:

before scope
variable (42) goes out of scope
after scope

Смотрите здесь

81
15.12.2008 15:38:35
Это намного аккуратнее, чем я думал, возможно!
elifiner 15.12.2008 16:23:05
Для информации эта функция поддерживается по крайней мере от GCC 4.0.0
Penghe Geng 6.06.2019 14:28:02
Это также работает в clang 11.0.0, хотя в справочнике атрибутов его нет.
j b 4.03.2020 09:14:49

Эй, вы пытаетесь воссоздать CFront !

0
15.12.2008 13:45:53
Не совсем. Cfront был компилятором, который брал код на C ++ и создавал код на C. Я ищу способы реализации конкретной идиомы непосредственно в C без предварительной фазы.
elifiner 15.12.2008 14:10:09
@gooli: Но Cfront должен был генерировать код C, который мог обрабатывать RAII, потому что код C ++, из которого он работал, требовал этого. Тем не менее, было сказано, что был момент, когда Cfront перестали быть пригодными для использования, и я считаю, что это было, когда на сцене появились исключения. Это может не иметь значения для вас.
Jonathan Leffler 15.12.2008 15:35:17
@Jonathan: сгенерированный код может просто вызывать «конструкторы» и «деструкторы» в начале и конце соответствующего блока. Поскольку код генерируется автоматически, в магии нет необходимости.
elifiner 15.12.2008 16:16:21

Я бы предпочел перезаписать адрес возврата в стеке. Это сработает как самый прозрачный. Замена freeбудет работать только с выделенными в куче «объектами».

1
15.12.2008 14:02:47

Вероятно, самый простой способ - использовать goto для перехода к метке в конце функции, но это, вероятно, слишком руководство для того, на что вы смотрите.

3
15.12.2008 15:33:31

Вы смотрели на alloca ()? Он освободится, когда переменная покинет область видимости. Но чтобы использовать его эффективно, вызывающая сторона всегда должна делать alloca перед его отправкой ... Если вы реализовали strdup, то вы не можете использовать alloca.

1
24.04.2009 07:02:16
alloca()на самом деле не чистый C. Это не соответствует ни одному стандарту C и поэтому не доступен везде, где доступен C. Например, он недоступен в компиляторах C Microsoft для Windows. Смотрите C FAQ .
hippietrail 19.03.2013 02:17:17
Он не освобождается, когда переменная покидает свою область видимости. Освобождается при выходе из функции, что делает его очень опасным.
Bauss 16.07.2017 22:53:53

Если ваш компилятор поддерживает C99 (или даже значительную его часть), вы можете использовать массив переменной длины (VLA), например:

int f(int x) { 
    int vla[x];

    // ...
}

Если память не изменяет, gcc имел / поддерживал эту функцию задолго до того, как она была добавлена ​​в C99. Это (примерно) эквивалентно простому случаю:

int f(int x) { 
    int *vla=malloc(sizeof(int) *x);
    /* ... */
    free vla;
}

Однако это не позволяет вам делать какие-либо другие действия, которые может выполнять dtor, такие как закрытие файлов, соединения с базой данных и т. Д.

9
8.07.2010 19:55:13
Помните, что 1) стек обычно гораздо более ограничен, чем куча; 2) Вы в основном не можете восстановиться после переполнения стека (вы получите SIGSEGV, который вы не можете обработать). Сбой malloc вернет nullptr, а сбой new выдаст std :: bad_alloc.
Raúl Salinas-Monteagudo 28.02.2018 12:13:46
@ RaúlSalinas-Monteagudo, что вы подразумеваете под "гораздо более ограниченным"?
j b 4.03.2020 09:04:30
@jb: в зависимости от операционной системы размер стека часто ограничен несколькими мегабайтами или около того. Но он только наполовину прав. Например, в Linux неудачный malloc (или новый) часто приводит к запуску OOMKILLER, который может просто убить рассматриваемую программу (или может убить другие программы, чтобы освободить достаточно памяти для успешного распределения). Таким образом, хотя куча часто больше, попытка использовать больше, чем у нее есть, также может быть необратима.
Jerry Coffin 5.03.2020 00:40:44
@jb: Если я правильно помню, размер стека (по умолчанию) составляет 8 МБ для обычного процесса и 2 МБ для потока. Если вы начнете создавать массивы в стеке, вы можете переполнить его. Но это зависит, конечно, от характера вашей даты и от того, насколько глубоко вы вкладываете заклинания. Я бы не стал вдаваться в такие опасности для надежной программы.
Raúl Salinas-Monteagudo 10.03.2020 08:07:48
@ RaúlSalinas-Monteagudo хорошие моменты. 8 МБ (или даже 2 МБ) на самом деле достаточно много памяти для большинства приложений, и я должен признать, что у меня никогда не было проблем. Тем не менее, я только что написал простую тестовую программу и смог ее мгновенно завершить, выделив в стеке массив размером 10 МБ. Обязательно учту это на будущее. Спасибо!
j b 10.03.2020 08:41:27

Одним из решений для перевода RAII в C (если у вас его нет cleanup()) является завершение вызова функции кодом, который будет выполнять очистку. Это также может быть упаковано в аккуратный макрос (показано в конце).

/* Publicly known method */
void SomeFunction() {
  /* Create raii object, which holds records of object pointers and a
     destruction method for that object (or null if not needed). */
  Raii raii;
  RaiiCreate(&raii);

  /* Call function implementation */
  SomeFunctionImpl(&raii);

  /* This method calls the destruction code for each object. */
  RaiiDestroyAll(&raii);
}

/* Hidden method that carries out implementation. */
void SomeFunctionImpl(Raii *raii) {
  MyStruct *object;
  MyStruct *eventually_destroyed_object;
  int *pretend_value;

  /* Create a MyStruct object, passing the destruction method for
     MyStruct objects. */
  object = RaiiAdd(raii, MyStructCreate(), MyStructDestroy);

  /* Create a MyStruct object (adding it to raii), which will later
     be removed before returning. */
  eventually_destroyed_object = RaiiAdd(raii,
      MyStructCreate(), MyStructDestroy);

  /* Create an int, passing a null destruction method. */
  pretend_value = RaiiAdd(raii, malloc(sizeof(int)), 0);

  /* ... implementation ... */

  /* Destroy object (calling destruction method). */
  RaiiDestroy(raii, eventually_destroyed_object);

  /* or ... */
  RaiiForgetAbout(raii, eventually_destroyed_object);
}

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

Например:

/* Declares Matrix * MatrixMultiply(Matrix * first, Matrix * second, Network * network) */
RTN_RAII(Matrix *, MatrixMultiply, Matrix *, first, Matrix *, second, Network *, network, {
  Processor *processor = RaiiAdd(raii, ProcessorCreate(), ProcessorDestroy);
  Matrix *result = MatrixCreate();
  processor->multiply(result, first, second);
  return processor;
});

void SomeOtherCode(...) {
  /* ... */
  Matrix * result = MatrixMultiply(first, second, network);
  /* ... */
}

Примечание: вы хотели бы использовать расширенную структуру макросов, такую ​​как P99, чтобы сделать что-то подобное вышеописанным возможным.

11
31.10.2018 19:09:47
Необходимость явного вызова метода ( RaiiDestroyAll) противоречит самой идее raii.
Mooing Duck 24.10.2018 17:14:17
Это механизм через язык. Если вы хотите, вы можете скрыть явный вызов с помощью макроса, например, RTN_RAII(int, func_name, int, arg0, int, arg1, {/* code */})(вы можете использовать P99, чтобы сделать тяжелый подъем макроса).
Keldon Alleyne 31.10.2018 14:33:18
ru.cppreference.com/w/cpp/language/raii «Другое название этого метода - Scope-Bound Resource Management (SBRM) после базового варианта использования, когда время жизни объекта RAII заканчивается из-за выхода из области». Бьярн Страуструп сказал: «RAII - это плохое имя для концепции ... Лучшее название, вероятно,: Constructor Acquires, Releases Destructor». Дело в том, что релиз является автоматическим, несмотря ни на что. Дело в том, что вы не должны делать уборку. Это определение RAII. Я слышал, что некоторые компиляторы C предлагают нечто похожее на расширение, но сам C не может этого сделать.
Mooing Duck 31.10.2018 17:06:20
Очевидно, что C не может выполнить автоматическую очистку на основе области. Из вопроса: «Я предполагаю, что это невозможно каким-либо вменяемым способом, но, возможно, возможно ли это с помощью какого-то подвоха». То, что я предоставил, представляет собой механизм обхода, при котором вы можете получить управляемую очистку в C с помощью простого макроса, где вам не нужно будет вручную вызывать destroy или cleanup (очистка будет выполняться в макросе), но вы нужно будет зарегистрировать указатели.
Keldon Alleyne 31.10.2018 19:09:50
Даже в C ++ требуется, чтобы вы как-то отмечали конец области видимости, обычно с помощью закрывающей скобки. Мне кажется, что RaiiDestroyAll () просто служит той же цели, за исключением того, что он не связан с закрывающей скобкой. Можно было бы повторно связать их с помощью макроса, но я не думаю, что это сделало бы все лучше: в сущности C ++ и C скрывать и не скрывать детали реализации, поэтому мне кажется, что это подходит подход (а не подвох) к RAII в C. А отдельная скобка и область видимости RAII позволяют вам делать такие вещи, как, например, использование одной области видимости RAII над несколькими скобками.
michaeljt 23.11.2018 16:36:19

Проверьте https://github.com/psevon/exceptions-and-raii-in-c для реализации C уникальных и общих smartpointers и исключений. Эта реализация опирается на макрос-скобки BEGIN ... END, заменяющие скобки и выявляющие умные указатели, выходящие за рамки видимости, а также замену макросов для возврата.

0
7.04.2014 19:55:41

Я не знал об очистке атрибутов раньше. Конечно, это хорошее решение, где оно применимо, но, похоже, оно плохо работает с реализациями исключений на основе setjmp / longjmp; функция очистки не вызывается для каких-либо промежуточных областей / функций между областью, которая вызвала исключение, и областью, которая его перехватывает. У Alloca такой проблемы нет, но с помощью alloca вы не можете перенести владение куском памяти во внешнюю область из вызывающей его функции, поскольку память выделяется из фрейма стека. Можно реализовать умные указатели, в некоторой степени похожие на C ++ unique_ptr и shared_ptr, хотя для них требуется использовать макрос-скобки вместо {} и return, чтобы иметь возможность связать дополнительную логику с областью входа / выхода. См. Autocleanup.c в https://github.com/psevon/exceptions-and-raii-in-c. для реализации.

0
8.04.2014 17:11:19

Чтобы дополнить эту часть ответа Йоханнеса:

атрибут cleanup запускает функцию, когда переменная выходит из области видимости

Существует ограничение на атрибут очистки ( http://gcc.gnu.org/onlinedocs/gcc-4.0.4/gcc/Variable-Attributes.html ): этот атрибут можно применять только к переменным области действия автоматической функции.

Поэтому, если в файле есть статическая переменная, можно реализовать RAII для статической переменной следующим образом:

#include <stdio.h>
#include <stdlib.h>

static char* watched2;

__attribute__((constructor))
static void init_static_vars()
{
  printf("variable (%p) is initialazed, initial value (%p)\n", &watched2, watched2);
  watched2=malloc(1024);
}


__attribute__((destructor))
static void destroy_static_vars()
{
  printf("variable (%p), value( %p) goes out of scope\n", &watched2, watched2);
  free(watched2);
}

int main(void)
{
  printf("exit from main, variable (%p) value(%p) is static\n", &watched2, watched2);
  return 0;
}

Это тест:

>./example
variable (0x600aa0) is initialazed, initial value ((nil))
exit from main, variable (0x600aa0) value(0x16df010) is static
variable (0x600aa0), value( 0x16df010) goes out of scope
0
25.04.2014 10:17:26
my implementation of raii for c in pure c and minimal asm
@ https://github.com/smartmaster/sml_clang_raii

**RAII for C language in pure C and ASM**

**featurs : **

-easy and graceful to use
- no need seperate free cleanup functions
- able to cleanup any resources or call any function on scope exits


**User guide : **

-add source files in src folder to your project
-include sml_raii_clang.h in.c file
-annote resource and its cleanup functions

/* образец кода */

void sml_raii_clang_test()
{
    //start a scope, the scope name can be any string
    SML_RAII_BLOCK_START(0);


    SML_RAII_VOLATILE(WCHAR*) resA000 = calloc(128, sizeof(WCHAR)); //allocate memory resource
    SML_RAII_START(0, resA000); //indicate starting a cleanup code fragment, here 'resA000' can be any string you want
    if (resA000) //cleanup code fragment
    {
        free(resA000);
        resA000 = NULL;
    }
    SML_RAII_END(0, resA000); //indicate end of a cleanup code fragment


    //another resource
    //////////////////////////////////////////////////////////////////////////
    SML_RAII_VOLATILE(WCHAR*) res8000 = calloc(128, sizeof(WCHAR));
    SML_RAII_START(0, D000);
    if (res8000)
    {
        free(res8000);
        res8000 = NULL;
    }
    SML_RAII_END(0, D000);


    //scope ended, will call all annoated cleanups
    SML_RAII_BLOCK_END(0);
    SML_RAII_LABEL(0, resA000); //if code is optimized, we have to put labels after SML_RAII_BLOCK_END
    SML_RAII_LABEL(0, D000);
}
0
28.04.2019 16:49:27