Типичные безопасные типовые структуры данных в простом старом C? - PullRequest
50 голосов
/ 14 июня 2010

Я сделал гораздо больше программирования на С ++, чем программирование на "простом старом С".Одна вещь, которую мне очень не хватает при программировании на простом C, - это типичные безопасные типовые структуры данных, которые предоставляются в C ++ с помощью шаблонов.

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

В C я могу придумать несколько способов реализации универсального односвязного списка:

  1. Запишите тип (ы) связанного списка и поддерживающие процедуры один раз, используя указатели void для обхода системы типов.
  2. Напишите макросы препроцессора, взяв необходимые имена типов и т. Д., Чтобы сгенерироватьспецифичная для типа версия структуры данных и вспомогательных процедур.
  3. Используйте более сложный, автономный инструмент для генерации кода для нужных вам типов.

Я неКак вариант 1, он подрывает систему типов и, вероятно, будет иметь худшую производительность, чем специализированная реализация, специфичная для типа.Использование унифицированного представления структуры данных для всех типов и приведение к / из указателей void, насколько я вижу, требует косвенного обращения, которого можно было бы избежать с помощью реализации, специализированной для типа элемента.

Option2 не требует каких-либо дополнительных инструментов, но выглядит несколько неуклюже и может привести к ошибкам компилятора при неправильном использовании.

Опция 3 может выдавать сообщения об ошибках компилятора лучше, чем опция 2, поскольку код специализированной структуры данных будетнаходиться в развернутом виде, который может быть открыт в редакторе и проверен программистом (в отличие от кода, сгенерированного макросами препроцессора).Однако этот вариант является наиболее тяжелым, своего рода «шаблонами бедняков».Я использовал этот подход раньше, используя простой сценарий sed, чтобы специализировать «шаблонную» версию некоторого кода C.

Я хотел бы программировать свои будущие «низкоуровневые» проекты на C, а не на C ++, нобыли напуганы мыслью переписать общие структуры данных для каждого конкретного типа.

Какой опыт у людей с этой проблемой?Существуют ли хорошие библиотеки общих структур данных и алгоритмов в C, которые не идут с Вариантом 1 (т.е. приведение к указателям void и из них, что жертвует безопасностью типов и добавляет уровень косвенности)?

Ответы [ 9 ]

22 голосов
/ 14 июня 2010

Вариант 1 - подход, принятый большинством реализаций C универсальных контейнеров, которые я вижу. Набор драйверов Windows и ядро ​​Linux используют макрос, позволяющий встраивать ссылки для контейнеров в любое место структуры, причем макрос используется для получения указателя структуры из указателя на поле ссылки:

Вариант 2 - тактика реализации контейнера BSD для tree.h и queue.h:

Не думаю, что я мог бы считать один из этих подходов безопасным. Полезно, но не безопасно типа.

19 голосов
/ 03 мая 2015

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

Красота C во многом объясняется отсутствием безопасности типов, работой вокруг системы типов и необработанным уровнем битов и байтов.Из-за этого есть некоторые вещи, которые он может сделать более легко без борьбы с языком, например, структуры переменной длины, использование стека даже для массивов, размеры которых определяются во время выполнения и т. Д. Это также имеет тенденцию быть намного прощесохраняйте ABI, когда вы работаете на этом более низком уровне.

Таким образом, здесь задействован другой вид эстетики, а также различные проблемы, и я бы рекомендовал изменить мышление, когда вы работаете в C.цените это, я бы предложил делать то, что многие люди считают само собой разумеющимся в наши дни, например, реализовывать свой собственный распределитель памяти или драйвер устройства.Когда вы работаете на таком низком уровне, вы не можете не рассматривать все как схемы памяти битов и байтов, в отличие от «объектов» с прикрепленным поведением.Кроме того, в таком низкоуровневом коде управления битами / байтами может возникнуть ситуация, когда C становится легче для понимания, чем код C ++, усеянный reinterpret_casts, например

Что касается вашего примера связанного списка, я бы предложилнеинтрузивная версия связанного узла (который не требует хранения указателей списка на сам тип элемента T, позволяющий отделить логику и представление связанного списка от самого T), например:

struct ListNode
{
    struct ListNode* prev;
    struct ListNode* next;
    MAX_ALIGN char element[1]; // Watch out for alignment here.
                               // see your compiler's specific info on 
                               // aligning data members.
};

Теперь мы можем создать узел списка следующим образом:

struct ListNode* list_new_node(int element_size)
{
    // Watch out for alignment here.
    return malloc_max_aligned(sizeof(struct ListNode) + element_size - 1);
}

// create a list node for 'struct Foo'
void foo_init(struct Foo*);
struct ListNode* foo_node = list_new_node(sizeof(struct Foo));
foo_init(foo_node->element);

Чтобы извлечь элемент из списка как T *:

T* element = list_node->element;

Поскольку это C,нет никакой проверки типов при приведении указателей таким образом, и это, вероятно, также даст вам неприятное ощущение, если вы пришли из C ++ фона.

Сложная часть здесь - убедиться, что этот член,element, правильно выровнен для любого типа, который вы хотите сохранить.Когда вы сможете решить эту проблему настолько мобильно, насколько это необходимо, у вас будет мощное решение для создания эффективных макетов и распределителей памяти.Зачастую это приводит к тому, что вы просто используете максимальное выравнивание для всего, что может показаться расточительным, но обычно это не так, если вы используете соответствующие структуры данных и распределители, которые не платят эти накладные расходы за многочисленные мелкие элементы в отдельности.

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

И я бы порекомендовал этот простой способ достижения универсальности вС во многих случаях.Просто замените T буфером, длина которого соответствует sizeof(T) и выровнена правильно.Если у вас есть достаточно переносимый и безопасный способ обобщения для обеспечения правильного выравнивания, у вас будет очень мощный способ работы с памятью, который часто улучшает попадания в кэш, уменьшает частоту выделения / освобождения кучи, количествотребуется перенаправление, время сборки и т. д.

Если вам нужна дополнительная автоматизация, например автоматическая инициализация list_new_node, struct Foo, я бы порекомендовал создать структуру таблицы общего типа, которую вы можете передавать, которая содержит информацию о том, насколько великаT - это указатель функции, указывающий на функцию для создания экземпляра T по умолчанию, другой для копирования T, клонирования T, уничтожения T, компаратора и т. Д. В C ++ вы можете генерировать эту таблицу автоматически, используя шаблоны и встроенный язык.такие понятия, как копировать конструкторы и деструкторы.C требует немного больше ручного усилия, но вы все равно можете немного уменьшить его с помощью макросов.

Еще одна хитрость, которая может быть полезна, если вы выбираете более макроориентированный маршрут генерации кода, заключается в том, чтобы воспользоваться соглашением об именовании идентификаторов на основе префикса или суффикса. Например, CLONE (Type, ptr) может быть определен для возврата Type##Clone(ptr), поэтому CLONE(Foo, foo) может вызвать FooClone(foo). Это своего рода обман, чтобы получить что-то похожее на перегрузку функций в C, и он полезен при генерации кода массовым образом (когда CLONE используется для реализации другого макроса) или даже при копировании и вставке кода типа шаблона, по крайней мере, улучшить равномерность шаблона.

3 голосов
/ 14 июня 2010

Вариант 1, использующий void * или какой-либо вариант на основе union, - это то, что используется большинством программ на С, и он может дать вам ЛУЧШУЮ производительность, чем стиль C ++ / macro, если иметь несколько реализаций для разных типов, поскольку он имеет меньше дублирование кода и, следовательно, меньшее давление icache и меньше промахов icache.

2 голосов
/ 15 июня 2010

GLib - это набор общих структур данных, http://www.gtk.org/

CCAN имеет кучу полезных фрагментов и таких http://ccan.ozlabs.org/

1 голос
/ 27 июля 2015

Я использую указатели void (void *) для представления общих структур данных, определенных с помощью struct и typedefs.Ниже я поделюсь своей реализацией библиотеки, над которой я работаю.

С помощью такой реализации вы можете думать о каждом новом типе, определенном с помощью typedef, как о псевдоклассе.Здесь этот псевдокласс является набором исходного кода (some_type_implementation.c) и его заголовочного файла (some_type_implementation.h).

В исходном коде вы должны определить структуру, которая будет представлять новый тип.Обратите внимание на структуру в исходном файле "node.c".Там я сделал недействительный указатель на атрибут info.Этот указатель может нести указатель любого типа (я думаю), но цена, которую вы должны заплатить, - это идентификатор типа внутри структуры (тип int), и все переключатели, чтобы сделать обработчик propper каждого типа определенным.Итак, в заголовочном файле node.h я определил тип «Node» (просто чтобы избежать необходимости каждый раз вводить struct node), а также мне нужно было определить константы «EMPTY_NODE», «COMPLEX_NODE» и «MATRIX_NODE".

Вы можете выполнить компиляцию вручную, используя" gcc * .c -lm ".

main.c Исходный файл

#include <stdio.h>
#include <math.h>

#define PI M_PI

#include "complex.h"
#include "matrix.h"
#include "node.h" 


int main()
{
    //testCpx();
    //testMtx();
    testNode();

    return 0;
}

node.cИсходный файл

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

#include "node.h"
#include "complex.h"
#include "matrix.h"

#define PI M_PI


struct node
{
    int type;

    void* info;
};


Node* newNode(int type,void* info)
{
    Node* newNode = (Node*) malloc(sizeof(Node));

    newNode->type = type;

    if(info != NULL)
    {
        switch(type)
        {
            case COMPLEX_NODE:
                newNode->info = (Complex*) info;
            break;

            case MATRIX_NODE:
                newNode->info = (Matrix*) info;
            break;
        }
    }
    else
        newNode->info = NULL;

    return newNode;
}

int emptyInfoNode(Node* node)
{
    return (node->info == NULL);
}

void printNode(Node* node)
{
    if(emptyInfoNode(node))
    {
        printf("Type:%d\n",node->type);
        printf("Empty info\n");
    }
    else
    {
        switch(node->type)
        {
            case COMPLEX_NODE:
                printCpx(node->info);
            break;

            case MATRIX_NODE:
                printMtx(node->info);
            break;
        }
    }
}

void testNode()
{
    Node *node1,*node2, *node3;
    Complex *Z;
    Matrix *M;

    Z = mkCpx(POLAR,5,3*PI/4);

    M = newMtx(3,4,PI);

    node1 = newNode(COMPLEX_NODE,Z);
    node2 = newNode(MATRIX_NODE,M);
    node3 = newNode(EMPTY_NODE,NULL);



    printNode(node1);
    printNode(node2);
    printNode(node3);
}

node.h Заголовочный файл

#define EMPTY_NODE   0
#define COMPLEX_NODE 1
#define MATRIX_NODE  2


typedef struct node Node;


Node* newNode(int type,void* info);
int emptyInfoNode(Node* node);
void printNode(Node* node);
void testNode();

matrix.c Исходный файл

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

#include "matrix.h"

struct matrix
{
    // Meta-information about the matrix 
    int rows;
    int cols;

    // The elements of the matrix, in the form of a vector 
    double** MTX;
};

Matrix* newMtx(int rows,int cols,double value)
{
    register int row , col;
    Matrix* M = (Matrix*)malloc(sizeof(Matrix));

    M->rows = rows;
    M->cols = cols;
    M->MTX = (double**) malloc(rows*sizeof(double*));

    for(row = 0; row < rows ; row++)
    {
        M->MTX[row] = (double*) malloc(cols*sizeof(double));

        for(col = 0; col < cols ; col++) 
            M->MTX[row][col] = value;
    }

    return M;
}

Matrix* mkMtx(int rows,int cols,double** MTX)
{   
    Matrix* M;
    if(MTX == NULL)
    {
        M = newMtx(rows,cols,0);
    }
    else
    {
        M = (Matrix*)malloc(sizeof(Matrix));
        M->rows = rows;
        M->cols = cols;
        M->MTX  = MTX;
    }
    return M;
}

double getElemMtx(Matrix* M , int row , int col)
{
    return M->MTX[row][col];
}

void printRowMtx(double* row,int cols)
{
    register int j;
    for(j = 0 ; j < cols ; j++) 
        printf("%g ",row[j]);           
}

void printMtx(Matrix* M)
{
    register int row = 0, col = 0;

    printf("\vSize\n");
    printf("\tRows:%d\n",M->rows);
    printf("\tCols:%d\n",M->cols);
    printf("\n");
    for(; row < M->rows ; row++)
    {
        printRowMtx(M->MTX[row],M->cols);
        printf("\n");
    }

    printf("\n");
}

void testMtx()
{
    Matrix* M = mkMtx(10,10,NULL);
    printMtx(M);
}

matrix.h Заголовочный файл

typedef struct matrix Matrix;

Matrix* newMtx(int rows,int cols,double value);
Matrix* mkMatrix(int rows,int cols,double** MTX);
void print(Matrix* M);
double getMtx(Matrix* M , int row , int col);
void printRowMtx(double* row,int cols);
void printMtx(Matrix* M);
void testMtx();

Исходный файл complex.c

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

#include "complex.h"

struct complex
{
    int type;

    double a;
    double b;
};

Complex* mkCpx(int type,double a,double b)
{
    /** Doc - {{{
     * This function makes a new Complex number.
     * 
     * @params:
     * |-->type: Is an interger that denotes if the number is in
     * |         the analitic or in the polar form.
     * |         ANALITIC:0
     * |         POLAR   :1
     * |
     * |-->a: Is the real part if type = 0 and is the radius if 
     * |      type = 1
     * |
     * `-->b: Is the imaginary part if type = 0 and is the argument
     *        if type = 1
     * 
     * @return:
     *      Returns the new Complex number initialized with the values 
     *      passed
     *}}} */

    Complex* number = (Complex*)malloc(sizeof(Complex));

    number->type = type;
    number->a    = a;
    number->b    = b;

    return number;
}

void printCpx(Complex* number)
{
    switch(number->type)
    {
        case ANALITIC:
            printf("Re:%g | Im:%g\n",number->a,number->b);
        break;

        case POLAR:
            printf("Radius:%g | Arg:%g\n",number->a,number->b);
        break;
    }
}

void testCpx()
{
    Complex* Z = mkCpx(ANALITIC,3,2);
    printCpx(Z);
}

Файл заголовка complex.h

#define ANALITIC 0 
#define POLAR    1 

typedef struct complex Complex;

Complex* mkCpx(int type,double a,double b);
void printCpx(Complex* number);
void testCpx();

Надеюсь, я ничего не пропустил.

1 голос
/ 16 июля 2013

Старый вопрос, я знаю, но в случае, если он все еще интересен: я экспериментировал с вариантом 2) (макросами препроцессора) сегодня и придумал пример, который я вставлю ниже. Немного неуклюжий, но не ужасный. Код не является полностью безопасным по типу, но содержит проверки работоспособности для обеспечения разумного уровня безопасности. И иметь дело с сообщениями об ошибках компилятора во время написания, было легко по сравнению с тем, что я видел, когда в игру вступили шаблоны C ++. Возможно, вам лучше всего начать читать это с примера использования кода в функции "main".

#include <stdio.h>

#define LIST_ELEMENT(type) \
    struct \
    { \
        void *pvNext; \
        type value; \
    }

#define ASSERT_POINTER_TO_LIST_ELEMENT(type, pElement) \
    do { \
        (void)(&(pElement)->value  == (type *)&(pElement)->value); \
        (void)(sizeof(*(pElement)) == sizeof(LIST_ELEMENT(type))); \
    } while(0)

#define SET_POINTER_TO_LIST_ELEMENT(type, pDest, pSource) \
    do { \
        ASSERT_POINTER_TO_LIST_ELEMENT(type, pSource); \
        ASSERT_POINTER_TO_LIST_ELEMENT(type, pDest); \
        void **pvDest = (void **)&(pDest); \
        *pvDest = ((void *)(pSource)); \
    } while(0)

#define LINK_LIST_ELEMENT(type, pDest, pSource) \
    do { \
        ASSERT_POINTER_TO_LIST_ELEMENT(type, pSource); \
        ASSERT_POINTER_TO_LIST_ELEMENT(type, pDest); \
        (pDest)->pvNext = ((void *)(pSource)); \
    } while(0)

#define TERMINATE_LIST_AT_ELEMENT(type, pDest) \
    do { \
        ASSERT_POINTER_TO_LIST_ELEMENT(type, pDest); \
        (pDest)->pvNext = NULL; \
    } while(0)

#define ADVANCE_POINTER_TO_LIST_ELEMENT(type, pElement) \
    do { \
        ASSERT_POINTER_TO_LIST_ELEMENT(type, pElement); \
        void **pvElement = (void **)&(pElement); \
        *pvElement = (pElement)->pvNext; \
    } while(0)

typedef struct { int a; int b; } mytype;

int main(int argc, char **argv)
{
    LIST_ELEMENT(mytype) el1;
    LIST_ELEMENT(mytype) el2;
    LIST_ELEMENT(mytype) *pEl;
    el1.value.a = 1;
    el1.value.b = 2;
    el2.value.a = 3;
    el2.value.b = 4;
    LINK_LIST_ELEMENT(mytype, &el1, &el2);
    TERMINATE_LIST_AT_ELEMENT(mytype, &el2);
    printf("Testing.\n");
    SET_POINTER_TO_LIST_ELEMENT(mytype, pEl, &el1);
    if (pEl->value.a != 1)
        printf("pEl->value.a != 1: %d.\n", pEl->value.a);
    ADVANCE_POINTER_TO_LIST_ELEMENT(mytype, pEl);
    if (pEl->value.a != 3)
        printf("pEl->value.a != 3: %d.\n", pEl->value.a);
    ADVANCE_POINTER_TO_LIST_ELEMENT(mytype, pEl);
    if (pEl != NULL)
        printf("pEl != NULL.\n");
    printf("Done.\n");
    return 0;
}
1 голос
/ 15 июня 2010

Существует распространенный вариант варианта 1, который более эффективен, так как использует объединения для хранения значений в узлах списка, т. Е. Нет дополнительной косвенности. Недостатком этого является то, что список принимает значения только определенных типов и потенциально тратит часть памяти, если типы имеют разные размеры.

Однако можно избавиться от union, используя вместо этого гибкий член массива, если вы хотите нарушить строгий псевдоним. Пример кода C99:

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

struct ll_node
{
    struct ll_node *next;
    long long data[]; // use `long long` for alignment
};

extern struct ll_node *ll_unshift(
    struct ll_node *head, size_t size, void *value);

extern void *ll_get(struct ll_node *head, size_t index);

#define ll_unshift_value(LIST, TYPE, ...) \
    ll_unshift((LIST), sizeof (TYPE), &(TYPE){ __VA_ARGS__ })

#define ll_get_value(LIST, INDEX, TYPE) \
    (*(TYPE *)ll_get((LIST), (INDEX)))

struct ll_node *ll_unshift(struct ll_node *head, size_t size, void *value)
{
    struct ll_node *node = malloc(sizeof *node + size);
    if(!node) assert(!"PANIC");

    memcpy(node->data, value, size);
    node->next = head;

    return node;
}

void *ll_get(struct ll_node *head, size_t index)
{
    struct ll_node *current = head;
    while(current && index--)
        current = current->next;
    return current ? current->data : NULL;
}

int main(void)
{
    struct ll_node *head = NULL;
    head = ll_unshift_value(head, int, 1);
    head = ll_unshift_value(head, int, 2);
    head = ll_unshift_value(head, int, 3);

    printf("%i\n", ll_get_value(head, 0, int));
    printf("%i\n", ll_get_value(head, 1, int));
    printf("%i\n", ll_get_value(head, 2, int));

    return 0;
}
1 голос
/ 14 июня 2010

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

0 голосов
/ 03 мая 2015

Я использую вариант 2 для пары высокопроизводительных коллекций, и это чрезвычайно трудоемко прорабатывает количество макропрограммы, необходимой для создания чего-то действительно общего во время компиляции и того, что стоит использовать.Я делаю это исключительно для сырой производительности (игры).Используется подход X-macros .

Болезненная проблема, которая постоянно возникает в варианте 2: «Предполагая некоторое конечное число опций, таких как бит 8/16/32/64ключи, я делаю указанное значение константой и определяю несколько функций, каждая из которых имеет свой элемент этого набора значений, который может принимать константа, или я просто превращаю его в переменную-член? "Первый означает менее производительный кэш инструкций, так как у вас есть много повторяющихся функций с одним или двумя разными числами, в то время как последний означает, что вы должны ссылаться на распределенные переменные, что в худшем случае означает пропуск кэша данных.Поскольку Вариант 1 является чисто динамическим, вы сделаете такие значения переменными-членами, даже не задумываясь об этом.Это действительно микрооптимизация.

Также имейте в виду компромисс между возвращаемыми указателями и значениями: последний наиболее эффективен, когда размер элемента данных меньше или равен размеру указателя.;тогда как, если элемент данных больше, скорее всего, лучше возвращать указатели, чем принудительно копировать большой объект, возвращая значение.

Я бы настоятельно рекомендовал перейти к варианту 1 в любом сценарии, в котором вы не находитесь.100% уверены, что производительность коллекции будет вашим узким местом.Даже с моим использованием Варианта 2, моя библиотека коллекций предоставляет «быструю настройку», которая аналогична Варианту 1, то есть использование значений void * в моем списке и карте.Этого достаточно для 90 +% обстоятельств.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...