11

たくさんの型を適切な大きさのchar配列にパックし、正しく型付けされた個々の参照としてデータにアクセスできるようにするクラステンプレートを作成しようとしています。現在、標準によれば、これは厳密なエイリアシング違反につながる可能性char[]があり、互換性のないオブジェクトを介してデータにアクセスしているため、未定義の動作につながる可能性があります。具体的には、標準は次のように述べています。

プログラムが次のタイプのいずれか以外のglvalueを介してオブジェクトの保存された値にアクセスしようとした場合、動作は未定義です。

  • オブジェクトの動的タイプ、
  • オブジェクトの動的タイプのcv修飾バージョン。
  • オブジェクトの動的タイプに類似したタイプ(4.4で定義)、
  • オブジェクトの動的型に対応する符号付きまたは符号なしの型である型、
  • オブジェクトの動的型のcv修飾バージョンに対応する符号付きまたは符号なし型である型。
  • 要素または非静的データメンバー(再帰的に、サブアグリゲートまたは含まれるユニオンの要素または非静的データメンバーを含む)の中に前述のタイプの1つを含む集合体または共用体タイプ。
  • オブジェクトの動的型の(おそらくcv修飾された)基本クラス型である型、
  • charまたはunsigned charタイプ。

強調表示された箇条書きの文言を考えると、私は次のalias_castアイデアを思いつきました。

#include <iostream>
#include <type_traits>

template <typename T>
T alias_cast(void *p) {
    typedef typename std::remove_reference<T>::type BaseType;
    union UT {
        BaseType t;
    };
    return reinterpret_cast<UT*>(p)->t;
}

template <typename T, typename U>
class Data {
    union {
        long align_;
        char data_[sizeof(T) + sizeof(U)];
    };
public:
    Data(T t = T(), U u = U()) { first() = t; second() = u; }
    T& first() { return alias_cast<T&>(data_); }
    U& second() { return alias_cast<U&>(data_ + sizeof(T)); }
};


int main() {
    Data<int, unsigned short> test;
    test.first() = 0xdead;
    test.second() = 0xbeef;
    std::cout << test.first() << ", " << test.second() << "\n";
    return 0;
}

(上記のテストコード、特にクラスはアイデアの単なるデモンストレーションなので、どのように使用するかDataを指摘しないでください。テンプレートもcv修飾型を処理するように拡張する必要があり、安全にしか使用できません。配置要件が満たされている場合に使用されますが、このスニペットがアイデアを実証するのに十分であることを願っています。)std::pairstd::tuplealias_cast

このトリックはg++による警告を沈黙させ(でコンパイルした場合g++ -std=c++11 -Wall -Wextra -O2 -fstrict-aliasing -Wstrict-aliasing)、コードは機能しますが、これは厳密なエイリアシングベースの最適化をスキップするようにコンパイラーに指示する本当に有効な方法ですか?

有効でない場合、エイリアシングルールに違反せずに、このようなchar配列ベースの汎用ストレージクラスを実装するにはどうすればよいでしょうか。

alias_cast編集:を次のような単純なものに置き換えreinterpret_castます:

T& first() { return reinterpret_cast<T&>(*(data_ + 0)); }
U& second() { return reinterpret_cast<U&>(*(data_ + sizeof(T))); }

g ++でコンパイルすると、次の警告が生成されます。

aliastest-so-1.cpp:'T&Data :: first()[with T=int;のインスタンス化 U = short unsigned int]':aliastest-so-1.cpp:28:16:
ここから必要ですaliastest-so-1.cpp:21:58:警告:型のパンニングされたポインターを逆参照すると、厳密なエイリアスルールが破られます[- Wstrict-aliasing]

4

1 に答える 1

3

厳密な適合性に固執したい場合、共用体を使用することはほとんど良い考えではありません。アクティブなメンバーの読み取りに関しては、厳密な規則があります (これは 1 つだけです)。実装は、信頼できる動作のためのフックとしてユニオンを使用するのが好きであると言わざるを得ませんが、おそらくそれがあなたが求めているものです。その場合は、エイリアシング ルールに関する素晴らしい (そして長い) 記事を書いた Mike Acton に任せます。彼はユニオンを介したキャストについてコメントしています。

私の知る限り、これは char 型の配列をストレージとして扱う方法です。

// char or unsigned char are both acceptable
alignas(alignof(T)) unsigned char storage[sizeof(T)];
::new (&storage) T;
T* p = static_cast<T*>(static_cast<void*>(&storage));

これが機能するように定義されている理由T 、ここでのオブジェクトの動的タイプです。ストレージは、新しい式がTオブジェクトを作成したときに再利用されました。この操作により、オブジェクトの有効期間が暗黙的に終了しましたstorage(これは、単純な型unsigned charであるため、簡単に発生します)。

これは、リストされている明示的な例外の 1 つであるタイプstorage[0]の glvalue を介してオブジェクト値を読み取るため、eg を使用してオブジェクトのバイトを読み取ることもできます。unsigned char一方storage、別のまだ些細な要素タイプである場合でも、上記のスニペットを機能させることはできますが、実行することはできませんstorage[0]

スニペットを実用的なものにするための最後の要素は、ポインターの変換です。一般的な場合には適してreinterpret_castないことに注意してください。が標準レイアウトである場合は有効ですT(配置にも追加の制限があります) が、その場合、使用は私が行ったようにing viareinterpret_castと同等になります。特にストレージの使用が一般的なコンテキストで頻繁に発生することを考えると、最初にその形式を直接使用する方が理にかなっています。いずれにせよ、to および fromの変換は標準的な変換の 1 つ (明確な意味を持つ) であり、それらが必要です。static_castvoidvoidstatic_cast

ポインターの変換 (これは私の意見では最も弱いリンクであり、ストレージの再利用に関する議論ではありません) についてまったく心配している場合は、別の方法として次のことを行います。

T* p = ::new (&storage) T;

追跡したい場合は、ストレージに追加のポインターが必要です。

の使用を心からお勧めしますstd::aligned_storage

于 2012-06-13T20:02:49.320 に答える