web-dev-qa-db-ja.com

「C」ヘッダーファイルで宣言された静的関数

私にとっては、ソースファイル内の静的関数を定義および宣言するルールです。つまり、.cファイルです。

しかし、ごくまれに、ヘッダーファイルで宣言している人がいます。静的関数には内部リンケージがあるため、関数を宣言するヘッダーファイルを含めるすべてのファイルで静的関数を定義する必要があります。これはかなり奇妙に見え、静的なものとして宣言するときに通常必要なものとはかけ離れています。

一方、素朴な誰かがそれを定義せずにその関数を使おうとすると、コンパイラは不満を言うでしょう。だから、ある意味でこれを行うことは奇妙に聞こえても本当に危険ではありません。

私の質問は:

  • ヘッダーファイルで静的関数を宣言する問題は何ですか?
  • リスクは何ですか?
  • コンパイル時間にどのような影響がありますか?
  • 実行時にリスクはありますか?
18
miguel azevedo

最初に、あなたが説明する状況についての理解を明確にしたいと思います:ヘッダーには静的関数宣言が含まれていますが、Cファイルには定義、つまり関数のソースコードが含まれています。例えば

some.h:

static void f();
// potentially more declarations

some.c:

#include "some.h"
static void f() { printf("Hello world\n"); }
// more code, some of it potentially using f()

これがあなたが説明する状況である場合、私はあなたの発言で問題を取ります

静的関数には内部リンケージがあるため、関数が宣言されているヘッダーファイルを含めるすべてのファイルで静的関数を定義する必要があります。

関数を宣言しても、特定の翻訳単位で使用しない場合は、定義する必要はないと思います。 gccはそれを受け入れ、警告を出します。私が何かを見逃さない限り、標準はそれを禁止していないようです。これは、関数を使用しないが宣言にヘッダーを含む翻訳単位が未使用の定義を提供する必要がないため、シナリオで重要になる場合があります。


  • ヘッダーファイルで静的関数を宣言する問題は何ですか?
    やや珍しいです。通常、静的関数は1つのファイルでのみ必要な関数です。可視性を制限することで明示的にするために、静的と宣言されています。したがって、ヘッダーでそれらを宣言することはやや相反するものです。関数が同一の定義を持つ複数のファイルで実際に使用される場合、単一の定義で外部に作成する必要があります。 1つの翻訳単位のみが実際に使用する場合、宣言はヘッダーに属しません。

    したがって、考えられるシナリオの1つは、それぞれの翻訳単位の異なる実装に対して均一な関数シグネチャを保証することです。共通ヘッダーは、C(およびC++)での異なる戻り値のタイプのコンパイル時エラーにつながります。 異なるパラメータタイプは、Cでのみコンパイル時エラーを引き起こします(ただし、関数のオーバーロードのためC++ではそうではありません)。
  • リスクとは何ですか?
    あなたのシナリオにはリスクはありません。 (カプセル化の原則に違反する可能性のあるヘッダーに関数definitionも含めるのとは対照的に。)
  • コンパイル時間にどのような影響がありますか?
    関数宣言は小さく、その複雑さは低いため、ヘッダーに追加の関数宣言を持つオーバーヘッドは無視できる可能性があります。しかし、多くの翻訳単位の宣言に追加のヘッダーを作成して含める場合、ファイル処理のオーバーヘッドが大きくなる可能性があります(つまり、コンパイラーはヘッダーI/O)
  • ランタイムにリスクはありますか?
    表示されません。
13

これは上記の質問に対する答えではありませんが、ヘッダーファイルにstatic(または_static inline_)関数を実装する可能性のあるwhyを示しています。

個人的には、ヘッダーファイルでいくつかの関数staticを宣言する2つの正当な理由しか考えられません。


  1. ヘッダーファイルが、現在のコンパイル単位でのみ表示されるインターフェイスを完全に実装している場合

    これは非常にまれですが、たとえばいくつかのサンプルライブラリの開発中のある時点での教育的コンテキスト。または、最小限のコードで別のプログラミング言語とインターフェイスする場合。

    開発者は、ライブラリまたはインターフェイスの実装が簡単でほぼそうであり、コードサイズよりも(ヘッダーファイルを使用する開発者にとって)使いやすさが重要な場合に、そうすることを選択できます。これらの場合、ヘッダーファイルの宣言ではプリプロセッサマクロを使用することが多く、同じヘッダーファイルを複数回含めることができ、Cで何らかの粗雑なポリモーフィズムを提供します。

    実用的な例は次のとおりです。線形合同擬似乱数ジェネレーター用の足で自分で遊べます。実装はコンパイルユニットに対してローカルであるため、各コンパイルユニットはPRNGの独自のコピーを取得します。この例では、Cで粗雑な多型を実装する方法も示しています。

    prng32.h

    _#if defined(PRNG_NAME) && defined(PRNG_MULTIPLIER) && defined(PRNG_CONSTANT) && defined(PRNG_MODULUS)
    #define MERGE3_(a,b,c) a ## b ## c
    #define MERGE3(a,b,c) MERGE3_(a,b,c)
    #define NAME(name) MERGE3(PRNG_NAME, _, name)
    
    static uint32_t NAME(state) = 0U;
    
    static uint32_t NAME(next)(void)
    {
        NAME(state) = ((uint64_t)PRNG_MULTIPLIER * (uint64_t)NAME(state) + (uint64_t)PRNG_CONSTANT) % (uint64_t)PRNG_MODULUS;
        return NAME(state);
    }
    
    #undef NAME
    #undef MERGE3
    #endif
    
    #undef PRNG_NAME
    #undef PRNG_MULTIPLIER
    #undef PRNG_CONSTANT
    #undef PRNG_MODULUS
    _

    上記を使用した例、example-prng32.h

    _#include <stdlib.h>
    #include <stdint.h>
    #include <stdio.h>
    
    #define PRNG_NAME       glibc
    #define PRNG_MULTIPLIER 1103515245UL
    #define PRNG_CONSTANT   12345UL
    #define PRNG_MODULUS    2147483647UL
    #include "prng32.h"
    /* provides glibc_state and glibc_next() */
    
    #define PRNG_NAME       borland
    #define PRNG_MULTIPLIER 22695477UL
    #define PRNG_CONSTANT   1UL
    #define PRNG_MODULUS    2147483647UL
    #include "prng32.h"
    /* provides borland_state and borland_next() */
    
    int main(void)
    {
        int i;
    
        glibc_state = 1U;
        printf("glibc lcg: Seed %u\n", (unsigned int)glibc_state);
        for (i = 0; i < 10; i++)
            printf("%u, ", (unsigned int)glibc_next());
        printf("%u\n", (unsigned int)glibc_next());
    
        borland_state = 1U;
        printf("Borland lcg: Seed %u\n", (unsigned int)borland_state);
        for (i = 0; i < 10; i++)
            printf("%u, ", (unsigned int)borland_next());
        printf("%u\n", (unsigned int)borland_next());
    
        return EXIT_SUCCESS;
    }
    _

    __state_変数と_next()関数staticの両方をマークする理由は、このようにヘッダーファイルを含む各コンパイルユニットが変数と関数の独自のコピーを持っているためです。 -ここでは、PRNGの独自のコピー。もちろん、それぞれ個別にシードする必要があります。また、同じ値にシードされた場合、同じシーケンスが生成されます。

    複雑なプリプロセッサマクロシェナンガンになり、実装を必要以上に理解、保守、および修正することがはるかに難しくなるため、一般にCでのこのような多態性の試みは避けてください。

    ただし、exploringいくつかのアルゴリズムのパラメータ空間-ここのように 2ビット線形合同ジェネレータ の場合、これにより単一の検証する各ジェネレーターの実装。それらの間に実装の違いがないことを確認します。この場合でさえ、開発ツールに似ており、他の人が使用するために提供されている実装で見るべきものではないことに注意してください。


  1. ヘッダーが単純な_static inline_アクセサー関数を実装する場合

    プリプロセッサマクロは一般に、複雑な構造タイプにアクセスするコードを簡素化するために使用されます。 _static inline_関数は似ていますが、コンパイル時に型チェックも提供し、パラメーターを複数回参照できることを除いて(マクロで問題があります)。

    実用的な使用例の1つは、低レベルのPOSIX.1 I/Oを使用してファイルを読み取るための単純なインターフェイスです(_<unistd.h>_の代わりに_<fcntl.h>_および_<stdio.h>_を使用します)。 GNU Cの標準I/Oとして、実数(カスタムのfloat/doubleパーサーを使用)を含む非常に大きな(数十メガバイトからギガバイトの範囲)のテキストファイルを読み取るときに自分でこれを行いました。特に高速ではありません。

    たとえば、inbuffer.h

    _#ifndef   INBUFFER_H
    #define   INBUFFER_H
    
    typedef struct {
        unsigned char  *head;       /* Next buffered byte */
        unsigned char  *tail;       /* Next byte to be buffered */
        unsigned char  *ends;       /* data + size */
        unsigned char  *data;
        size_t          size;
        int             descriptor;
        unsigned int    status;     /* Bit mask */
    } inbuffer;
    #define INBUFFER_INIT { NULL, NULL, NULL, NULL, 0, -1, 0 }
    
    int inbuffer_open(inbuffer *, const char *);
    int inbuffer_close(inbuffer *);
    
    int inbuffer_skip_slow(inbuffer *, const size_t);
    int inbuffer_getc_slow(inbuffer *);
    
    static inline int inbuffer_skip(inbuffer *ib, const size_t n)
    {
        if (ib->head + n <= ib->tail) {
            ib->head += n;
            return 0;
        } else
            return inbuffer_skip_slow(ib, n);
    }
    
    static inline int inbuffer_getc(inbuffer *ib)
    {
        if (ib->head < ib->tail)
            return *(ib->head++);
        else
            return inbuffer_getc_slow(ib);
    }
    
    #endif /* INBUFFER_H */
    _

    上記のinbuffer_skip()およびinbuffer_getc()は、ibがNULLでないかどうかをチェックしないことに注意してください。これはそのような機能の典型です。これらのアクセサー関数は、「高速パスで」、つまり非常に頻繁に呼び出されると想定されます。このような場合、関数呼び出しのオーバーヘッドでさえ問題になります(呼び出しサイトのコードで複製されるため、_static inline_関数では回避されます)。

    上記のinbuffer_skip()inbuffer_getc()のような単純なアクセサー関数は、関数がパラメーターを特定のレジスターまたは上に配置することを期待するため、コンパイラーが関数呼び出しに関連するレジスター移動を回避することもできますインライン関数は、インライン関数を囲むコードに適合させることができます(レジスタの使用)。

    個人的には、まずインライン化されていない関数を使用していくつかのテストプログラムを作成し、パフォーマンスと結果をインライン化されたバージョンと比較することをお勧めします。結果を比較することで、インラインバージョンにバグがないことを確認し(ここでは1つのタイプがオフになっていることが一般的です!)、パフォーマンスと生成されたバイナリ(少なくともサイズ)を比較することで、インライン化が一般に価値があるかどうかがわかります。

10
Nominal Animal

なぜグローバル関数と静的関数の両方が必要なのですか? cでは、関数はデフォルトでグローバルです。静的関数は、宣言されているファイルへの関数へのアクセスを制限する場合にのみ使用します。したがって、静的と宣言することにより、積極的にアクセスを制限します...

ヘッダーファイルでの実装の唯一の要件は、c ++テンプレート関数とテンプレートクラスメンバー関数です。

0
JHBonarius