web-dev-qa-db-ja.com

昔ながらのCでのタイプセーフな汎用データ構造?

私は「プレーン・オールドC」プログラミングよりもはるかに多くのC++プログラミングを行ってきました。プレーンCでプログラミングするときに私が心から欠けているのは、タイプセーフな汎用データ構造です。これは、テンプレートを介してC++で提供されます。

明確にするために、一般的な単一リンクリストを検討してください。 C++では、独自のテンプレートクラスを定義して、必要な型に合わせてインスタンス化するのは簡単です。

Cでは、一般的な単一リンクリストを実装するいくつかの方法を考えることができます。

  1. リンクされたリストのタイプとサポートプロシージャを、ボイドポインターを使用して1回記述し、タイプシステムを移動します。
  2. 必要な型名などをとるプリプロセッサマクロを記述して、データ構造とサポートプロシージャの型固有のバージョンを生成します。
  3. より高度なスタンドアロンツールを使用して、必要なタイプのコードを生成します。

オプション1は、型システムを破壊するものであり、特殊な型固有の実装よりもパフォーマンスが低下する可能性があるため、私は好きではありません。すべてのタイプのデータ構造の統一表現を使用し、ボイドポインターへのキャスト/ボイドポインターからのキャストは、私の知る限り、エレメントタイプに特化した実装によって回避される間接参照を必要とします。

オプション2は追加のツールを必要としませんが、多少不格好な感じがします。不適切に使用すると、不適切なコンパイラエラーが発生する可能性があります。

特別なデータ構造コードは、(プリプロセッサマクロによって生成されたコードではなく)エディターで開いてプログラマーが検査できる拡張形式で存在するため、オプション3はオプション2よりも優れたコンパイラエラーメッセージを提供します。ただし、このオプションは最も重い、一種の「貧乏人のテンプレート」です。私は以前、このアプローチを使用しました。単純なsedスクリプトを使用して、いくつかのCコードの「テンプレート化された」バージョンを特殊化しました。

将来の「低レベル」プロジェクトをC++ではなくCでプログラミングしたいのですが、特定のタイプごとに共通のデータ構造を書き換えるという考えに怯えています。

この問題について人々はどのような経験をしましたか?オプション1に対応していないCの一般的なデータ構造とアルゴリズムの優れたライブラリー(つまり、型の安全性を犠牲にし、一定レベルの間接参照を追加するvoidポインターとの間のキャスト)

54
Brad Larsen

オプション1は、私が目にする汎用コンテナーのほとんどのC実装で採用されているアプローチです。 WindowsドライバーキットとLinuxカーネルは、マクロを使用してコンテナーのリンクを構造体の任意の場所に埋め込むことができ、マクロを使用してリンクフィールドへのポインターから構造体ポインターを取得します。

オプション2は、BSDのtree.hおよびqueue.hコンテナの実装で採用されている方法です。

これらのアプローチのいずれもタイプセーフだとは思いません。便利ですが、タイプセーフではありません。

22
Michael Burr

CにはC++とは異なる種類の美しさがあります。タイプセーフであり、デバッガーでキャストを使用せずにコードをトレースするときにすべてが何であるかを常に確認できることは、通常、その1つではありません。

Cの美しさは、型の安全性の欠如、型システムの回避、およびビットとバイトの生のレベルでの作業に多くをもたらします。そのため、たとえば、可変長の構造体、実行時にサイズが決定される配列に対してもスタックを使用するなど、言語と戦うことなくより簡単に実行できる特定の処理があります。また、この低いレベルで作業しているときにABIを保持します。

したがって、ここにはさまざまな種類の美学とさまざまな課題があります。Cで作業するときは、考え方を変えることをお勧めします。本当に感謝するために、最近、多くの人が当たり前だと思うことを行うことをお勧めします。独自のメモリアロケータまたはデバイスドライバーの実装。このような低レベルで作業しているときは、ビヘイビアが付加された「オブジェクト」ではなく、すべてをビットとバイトのメモリレイアウトとして見るしかありません。さらに、そのような低レベルのビット/バイト操作コードで、Cが_reinterpret_casts_が散らばっているC++コードよりも理解しやすくなる場合があります。

リンクリストの例については、非侵入バージョンのリンクノード(要素タイプ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(動的ポリモーフィズム)に対してそれを操作するための対応するロジックを用意する以外に、できることはほとんどありません。ただし、必要だったと思われるような追加の間接レベルは含まれず、リストノードと要素全体が1回の割り当てで割り当てられます。

そして、私は多くの場合Cで一般性を達成するためにこの単純な方法をお勧めします。 Tを、sizeof(T)に一致し、適切に配置された長さのバッファーに置き換えるだけです。適切な位置合わせを確実にするために一般化できる合理的に移植可能で安全な方法がある場合、キャッシュヒットを改善し、ヒープの割り当て/割り当て解除の頻度を減らす方法でメモリを操作する非常に強力な方法があります。間接指定が必要、ビルド時間など.

_list_new_node_が_struct Foo_を自動的に初期化するような自動化がさらに必要な場合は、Tの大きさ、関数を指す関数ポインタなどの情報を含む一般的な型テーブル構造体を作成することをお勧めしますTのデフォルトインスタンスを作成し、Tをコピーし、Tを複製し、Tを破壊し、Tを破壊し、コンパレーターなどを作成します。C++では、テンプレートと、コピーコンストラクターやデストラクターなどの組み込み言語の概念を使用して、このテーブルを自動的に生成できます。 Cは少し手作業が必要ですが、マクロを使用してボイラープレートを少し減らすことができます。

よりマクロ指向のコード生成ルートを使用する場合に役立つもう1つのトリックは、識別子の接頭辞または接尾辞ベースの命名規則を利用することです。たとえば、CLONE(Type、ptr)はType##Clone(ptr)を返すように定義できるため、CLONE(Foo, foo)FooClone(foo)を呼び出すことができます。これは、Cで関数のオーバーロードに似たものを得るためのチートの一種であり、コードを一括で生成する場合(CLONEを使用して別のマクロを実装する場合)、またはボイラープレート型のコードを少なくともコピーおよび貼り付けする場合にも役立ちますボイラープレートの均一性を向上させます。

19
Dragon Energy

オプション1は、void *または一部のunionベースのバリアントのいずれかを使用しており、ほとんどのCプログラムが使用します。これにより、さまざまなタイプの複数の実装を持つC++ /マクロスタイルよりもパフォーマンスが向上する場合があります。コードの重複が少ないため、icacheのプレッシャーが少なく、icacheミスが少なくなります。

3
Chris Dodd

GLibには一般的なデータ構造がたくさんあります http://www.gtk.org/

CCANには便利なスニペットなどがたくさんあります http://ccan.ozlabs.org/

2
Spudd86

構造体とtypedefで定義された一般的なデータ構造を表すためにvoidポインター(void *)を使用しています。以下に、私が取り組んでいるlibの実装を示します。

この種の実装では、typedefで定義されたそれぞれの新しい型を、疑似クラスのように考えることができます。ここで、この疑似クラスは、ソースコード(some_type_implementation.c)とそのヘッダーファイル(some_type_implementation.h)のセットです。

ソースコードでは、新しい型を表す構造体を定義する必要があります。 「node.c」ソースファイルの構造体に注意してください。そこで、「info」属性への無効なポインタを作成しました。このポインターは任意のタイプのポインターを運ぶ可能性がありますが(私は思う)、支払う必要がある価格は構造体(intタイプ)内のタイプ識別子であり、各タイプの適切なハンドルを定義するためのすべてのスイッチです。したがって、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
user4713908

オプション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
Christoph

あなたのオプション1は、cプログラマーがほとんどの時間を費やすことになるものであり、反復型入力を削減するために2を少し追加する可能性があり、いくつかの関数ポインタを多分多型のフレーバー。

1
dmckee

古い質問ですが、それでもなお関心がある場合は、今日オプション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
michaeljt

将来の「低レベル」プロジェクトをC++ではなくCでプログラムしたいと思います...

どうして?ターゲットにはC++コンパイラまたはC++ランタイムがありませんか?

0
John

いくつかの高性能コレクションにオプション2を使用していますが、本当にコンパイル時の汎用的な使用に値するマクロロジックを実行するために必要なマクロロジックの処理には、非常に時間がかかります。私は純粋に生のパフォーマンス(ゲーム)のためにこれを行っています。 X-macros アプローチが使用されます。

オプション2で常に発生する痛みを伴う問題は、「8/16/32/64ビットキーなどのいくつかの有限数のオプションを想定して、この値を定数にして、それぞれ異なる要素を持ついくつかの関数を定義することですか?定数が取ることができる値のセット、またはそれをメンバー変数にするだけですか?」前者は、1つまたは2つの数値が異なるだけの多くの繰り返し関数があるため、パフォーマンスの低い命令キャッシュを意味します。後者は、割り当てられた変数を参照する必要があることを意味し、最悪の場合、データキャッシュミスを意味します。オプション1は純粋に動的であるため、そのような値を何も考えずにメンバー変数にします。しかし、これは本当にマイクロ最適化です。

また、返されるポインタと値の間のトレードオフにも注意してください。後者は、データ項目のサイズがポインタのサイズ以下の場合に最もパフォーマンスが高くなります。一方、データ項目が大きい場合は、値を返すことによってラージオブジェクトのコピーを強制するよりも、ポインタを返す方が適切です。

コレクションのパフォーマンスがボトルネックになることが100%確実でない場合は、オプション1に進むことを強くお勧めします。オプション2を使用しても、コレクションライブラリはオプション1のような「クイックセットアップ」を提供します。つまり、void *リストとマップの値。これは状況の90 +%に十分です。

0
Engineer

あなたはチェックアウトすることができます https://github.com/clehner/ll.c

使い方は簡単です:

#include <stdio.h>
#include <string.h>
#include "ll.h"

int main()
{
   int *numbers = NULL;
   *( numbers = ll_new(numbers) ) = 100;
   *( numbers = ll_new(numbers) ) = 200;

   printf("num is %d\n", *numbers);
   numbers = ll_next(numbers);
   printf("num is %d\n", *numbers);

   typedef struct _s {
      char *Word;
   } s;

   s *string = NULL;
   *( string = ll_new(string) ) = (s) {"a string"};
   *( string = ll_new(string) ) = (s) {"another string"};

   printf("string is %s\n", string->Word);
   string = ll_next( string );
   printf("string is %s\n", string->Word);

   return 0;
}

出力:

num is 200
num is 100
string is another string
string is a string
0
mg979