web-dev-qa-db-ja.com

関数テンプレート内の静的ローカル変数のアドレスをタイプ識別子として使用しても安全ですか?

[〜#〜] rtti [〜#〜] を必要としない std::type_index の代替を作成したい:

template <typename T>
int* type_id() {
    static int x;
    return &x;
}

x自体の値ではなく、ローカル変数xのアドレスがタイプIDとして使用されることに注意してください。また、実際にはベアポインターを使用するつもりはありません。質問に関係のないものはすべて取り除いたところです。実際のtype_index実装 ここ を参照してください。

このアプローチは適切ですか、もしそうであれば、なぜですか?そうでない場合、なぜでしょうか?私はここで不安定な立場にいるように感じるので、私のアプローチが機能する、または機能しない正確な理由に興味があります。

典型的な使用例は、実行時にルーチンを登録して、単一のインターフェースを通じて異なるタイプのオブジェクトを処理することです。

class processor {
public:
    template <typename T, typename Handler>
    void register_handler(Handler handler) {
        handlers[type_id<T>()] = [handler](void const* v) {
            handler(*static_cast<T const*>(v));
        };
    }

    template <typename T>
    void process(T const& t) {
        auto it = handlers.find(type_id<T>());
        if (it != handlers.end()) {
            it->second(&t);
        } else {
            throw std::runtime_error("handler not registered");
        }
    }

private:
    std::map<int*, std::function<void (void const*)>> handlers;
};

このクラスは次のように使用できます:

processor p;

p.register_handler<int>([](int const& i) {
    std::cout << "int: " << i << "\n";
});
p.register_handler<float>([](float const& f) {
    std::cout << "float: " << f << "\n";
});

try {
    p.process(42);
    p.process(3.14f);
    p.process(true);
} catch (std::runtime_error& ex) {
    std::cout << "error: " << ex.what() << "\n";
}

結論

皆さんの助けに感謝します。 @StoryTellerからの回答を受け入れました。彼がソリューションすべきがC++のルールに従って有効である理由を概説したからです。ただし、@ SergeBallestaなどのコメントの多くは、MSVCが最適化を実行し、このアプローチを破ることに不快に近づいていることを指摘しています。より堅牢なアプローチが必要な場合は、@ [ガレージ]で提案されているように、std::atomicを使用した解決策が望ましい場合があります。

std::atomic_size_t type_id_counter = 0;

template <typename T>
std::size_t type_id() {
    static std::size_t const x = type_id_counter++;
    return x;
}

誰かがさらに考えや情報を持っているなら、私はそれをまだ聞きたいです!

40
Joseph Thomson

はい、ある程度は正しいでしょう。テンプレート関数は暗黙的にinlineであり、inline関数内の静的オブジェクトはすべての翻訳単位で共有されます。

したがって、すべての変換単位で、type_id<Type>()の呼び出しに対して同じ静的ローカル変数のアドレスを取得します。ここでは、標準によってODR違反から保護されています。

したがって、ローカル静的アドレスcanは、自家製のランタイムタイプ識別子の一種として使用されます。

C++はテンプレートを使用し、Javaのような型消去を伴うジェネリックスを使用しないため、宣言された各型には、静的変数を含む独自の関数実装があります。これらの変数はすべて異なり、したがって、異なるアドレスがあります。

問題は、それらのvalueが使用されず、さらに悪いことに変更されないことです。オプティマイザは文字列定数をマージできることを覚えています。オプティマイザーは人間のプログラマーよりはるかに賢いように最善を尽くすので、熱心すぎる最適化コンパイラーは、これらの変数の値は決して変更されないため、すべて0の値を維持することに気づくと思います。メモリを節約しますか?

As ifルールにより、観察可能な結果が同じであれば、コンパイラーは自由に実行できます。また、常に同じ値を共有する静的変数のaddressesが異なるかどうかはわかりません。たぶん誰かが標準のどの部分が実際にそれを気にかけているかを確認できましたか?

現在のコンパイラーは、引き続き個別のプログラム単位をコンパイルするため、別のプログラム単位が値を使用または変更するかどうかを確認できません。だから私の意見では、オプティマイザは変数をマージすることを決定するのに十分な情報を持っていないので、あなたのパターンは安全です。

しかし、標準で保護されているとは本当に思えないので、将来のバージョンのC++ビルダー(コンパイラ+リンカー)が、マージできる変更されていない変数を積極的に検索するグローバル最適化フェーズを生み出さないかどうかはわかりません。コードの一部を最適化するために積極的にUBを検索するのとほぼ同じです...それらを許可しないと大きすぎるコードベースを壊す可能性がある一般的なパターンのみが保護されます。私はあなたのパターンが十分に一般的であるとは思いません。

同じ値を持つ変数をマージする最適化フェーズを防ぐためのかなりハッキーな方法は、それぞれに異なる値を与えることです。

int unique_val() {
    static int cur = 0;  // normally useless but more readable
    return cur++;
}
template <typename T>
void * type_id() {
    static int x = unique_val();
    return &x;
}

わかりました、これはスレッドセーフになることさえ試みませんが、ここでは問題ではありません。値がそれ自体で使用されることは決してありません。しかし、これで、(@ StoryTellerで述べられている標準の14.8.2に従って)静的な期間を持つさまざまな変数があり、競合状態を除いて、さまざまな値があります。それらはodrとして使用されるので、それらは異なるアドレスを持つ必要があり、最適化コンパイラーの将来の改善から保護する必要があります...

注:この値は使用されないので、void *を返すときれいに聞こえます...


@bogdanからのコメントからstolenを追加しただけです。 MSVCは/OPT:ICFフラグを使用して非常に強力な最適化を行うことが知られています 。ディスカッションは、が適合であってはならず、constとしてマークされた変数にのみ適用されることを示唆しています。しかし、それは、OPのコードが適合しているように見えても、実稼働コードでは追加の予防策なしではあえてそれを使用しないだろうという私の意見を強化します。

11
Serge Ballesta

@ StoryTeller で言及されているように、実行時にうまく機能します。
次のように使用できないことを意味します:

template<int *>
struct S {};

//...

S<type_id<char>()> s;

さらに、それは固定識別子ではありません。したがって、charが実行可能ファイルの異なる実行を通じて同じ値にバインドされるという保証はありません。

これらの制限に対処できれば問題ありません。


永続的な識別子が必要なタイプがすでにわかっている場合は、代わりに次のようなものを使用できます(C++ 14の場合)。

template<typename T>
struct wrapper {
    using type = T;
    constexpr wrapper(std::size_t N): N{N} {}
    const std::size_t N;
};

template<typename... T>
struct identifier: wrapper<T>... {
    template<std::size_t... I>
    constexpr identifier(std::index_sequence<I...>): wrapper<T>{I}... {}

    template<typename U>
    constexpr std::size_t get() const { return wrapper<U>::N; }
};

template<typename... T>
constexpr identifier<T...> ID = identifier<T...>{std::make_index_sequence<sizeof...(T)>{}};

次のように識別子を作成します。

constexpr auto id = ID<int, char>;

他のソリューションと同じように、これらの識別子を使用できます。

handlers[id.get<T>()] = ...

さらに、定数式が必要な場合はどこでも使用できます。
テンプレートパラメータとしての例:

template<std::size_t>
struct S {};

// ...

S<id.get<B>()> s{};

Switchステートメントで:

    switch(value) {
    case id.get<char>():
         // ....
         break;
    case id.get<int>():
        // ...
        break;
    }
}

等々。また、IDのテンプレートパラメータリストでタイプの位置を変更しない限り、それらは異なる実行で永続的であることに注意してください。

主な欠点は、id変数を導入するときに、識別子が必要なすべてのタイプを知っている必要があることです。

6
skypjack

コメント後の編集:最初の読み取りでは、アドレスがint値ではなくキーとして使用されていることに気付きませんでした。これは賢いアプローチですが、IMHOに重大な欠陥があります。intentは、誰かがそのコードを見つけた場合、非常に不明確です。

古いCハックのようです。それは巧妙で効率的ですが、コードは意図が何であるかをまったく説明しません。最近のc ++では、imhoはどちらが悪いのでしょう。コンパイラーではなく、プログラマー向けのコードを記述します。ベアメタルの最適化を必要とする深刻なボトルネックがあることを証明していない限り。

それはうまくいくと思いますが、私は明らかに言語弁護士ではありません...

エレガントですが複雑なconstexprソリューションが見つかります here または here

元の答え

これは有効なc ++であり、静的ローカルが最初の関数呼び出しで初期化されるため、すべてのプログラムで返されたポインターにアクセスできるという意味で「安全」です。コードで使用されるT型ごとに1つの静的変数があります。

だが :

  • なぜ非constポインタを返すのですか?これにより、呼び出し元が静的変数の値を変更できるようになりますが、これは明らかにあなたが望むものではありません
  • Constポインターを返す場合、ポインターを返すのではなく、値で返さないことに興味はありません

また、型IDを取得するこの方法は、コンパイル時にのみ機能し、ポリモーフィックオブジェクトでは実行時に機能しません。したがって、基本参照またはポインターから派生クラス型を返すことはありません。

静的int値をどのように初期化しますか?ここではそれらを初期化しないため、これは無効です。多分あなたはどこかにそれらを初期化するために非定数ポインタを使用したいと思いましたか?

2つのより良い可能性があります。

1)サポートするすべてのタイプにテンプレートを特化します

template <typename T>
int type_id() {
    static const int id = typeInitCounter++;
    return id;
}

template <>
int type_id<char>() {
    static const int id = 0;
    return id;  //or : return 0
}

template <>
int type_id<unsigned int>() {
    static const int id = 1;
    return id;  //or : return 1
}

//etc...

2)グローバルカウンターを使用します

std::atomic<int> typeInitCounter = 0;

template <typename T>
int type_id() {
    static const int id = typeInitCounter++;
    return id;
}

タイプを管理する必要がないため、この最後のアプローチはIMHOの方が優れています。また、A.S.Hで指摘されているように、ゼロベースのインクリメントカウンターでは、vectorの代わりにmapを使用できます。これは、はるかにシンプルで効率的です。

また、unordered_mapmapの代わりに、注文する必要はありません。これにより、O(1) O(log(n))の代わりにアクセスできます)

6
galinette