20

n3242以降の3.10/10および3.11を明示的に考慮して、ISO-C ++ 11に準拠し、任意のPODタイプ間でエイリアスを作成する安全な方法が必要です。ここでは厳密なエイリアシングについて多くの質問がありますが、そのほとんどはC++ではなくCに関するものです。おそらくこのセクションを使用して、ユニオンを使用するCの「ソリューション」を見つけました

要素または非静的データメンバーの中に前述のタイプの1つを含む共用体タイプ

それから私はこれを作りました。

#include <iostream>

template <typename T, typename U>
T& access_as(U* p)
{
    union dummy_union
    {
        U dummy;
        T destination;
    };

    dummy_union* u = (dummy_union*)p;

    return u->destination;
}

struct test
{
    short s;
    int i;
};

int main()
{
    int buf[2];

    static_assert(sizeof(buf) >= sizeof(double), "");
    static_assert(sizeof(buf) >= sizeof(test), "");

    access_as<double>(buf) = 42.1337;
    std::cout << access_as<double>(buf) << '\n';

    access_as<test>(buf).s = 42;
    access_as<test>(buf).i = 1234;

    std::cout << access_as<test>(buf).s << '\n';
    std::cout << access_as<test>(buf).i << '\n';
}

私の質問は、確かに、このプログラムは標準に従って合法ですか?*

警告は一切表示されず、以下を使用してMinGW /GCC4.6.2でコンパイルすると正常に動作します。

g++ -std=c++0x -Wall -Wextra -O3 -fstrict-aliasing -o alias.exe alias.cpp

*編集:そうでない場合、これを合法的に変更するにはどうすればよいですか?

4

4 に答える 4

15

これは、奇妙なキャストやユニオンなどを使用してどのようなゆがみを実行しても、決して合法ではありません。

基本的な事実は次のとおりです。異なる型の 2 つのオブジェクトは、いくつかの特別な例外を除いて、メモリ内で決して別名になることはありません (さらに下を参照)。

次のコードを検討してください。

void sum(double& out, float* in, int count) {
    for(int i = 0; i < count; ++i) {
        out += *in++;
    }
}

これをローカル レジスタ変数に分割して、実際の実行をより厳密にモデル化しましょう。

void sum(double& out, float* in, int count) {
    for(int i = 0; i < count; ++i) {
        register double out_val = out; // (1)
        register double in_val = *in; // (2)
        register double tmp = out_val + in_val;
        out = tmp; // (3)
        in++;
    }
}

(1)、(2)、(3) がそれぞれメモリの読み取り、読み取り、書き込みを表しているとします。これは、このようなタイトな内部ループでは非常にコストのかかる操作になる可能性があります。このループの合理的な最適化は次のようになります。

void sum(double& out, float* in, int count) {
    register double tmp = out; // (1)
    for(int i = 0; i < count; ++i) {
        register double in_val = *in; // (2)
        tmp = tmp + in_val;
        in++;
    }
    out = tmp; // (3)
}

この最適化により、必要なメモリ読み取り数が半分になり、メモリ書き込み数が 1 に減少します。これは、コードのパフォーマンスに大きな影響を与える可能性があり、すべての最適化 C および C++ コンパイラにとって非常に重要な最適化です。

ここで、厳密なエイリアシングがないとします。任意のタイプのオブジェクトへの書き込みが他のオブジェクトに影響を与える可能性があるとします。double への書き込みがどこかの float の値に影響を与える可能性があるとします。これにより、上記の最適化が疑わしいものになります。なぜなら、プログラマーが実際に out と in にエイリアスを意図していた可能性があるため、 sum 関数の結果がより複雑になり、プロセスの影響を受ける可能性があるからです。ばかげているように聞こえますか?それでも、コンパイラは「愚かな」コードと「賢い」コードを区別できません。コンパイラは、整形式のコードと不正な形式のコードのみを区別できます。フリーエイリアシングを許可する場合、コンパイラは最適化において保守的である必要があり、ループの各反復で余分なストア (3) を実行する必要があります。

ユニオンやキャストのトリックが合法ではない理由が理解できたと思います。このような基本的な概念を巧妙に回避することはできません。

厳密なエイリアシングの例外

C および C++ 標準charでは、クラス メンバーのアドレスを独立して使用できることが非常に重要であるため、派生型、基本型、およびメンバーを含む「関連型」を使用して、任意の型をエイリアス化するための特別な規定が設けられています。この回答では、これらの規定の完全なリストを見つけることができます。

さらに、GCC は、最後に書き込まれたものとは異なる共用体のメンバーから読み取るための特別な規定を作成します。この種のユニオンによる変換では、実際にはエイリアシングに違反することはできないことに注意してください。一度にアクティブにできる共用体のメンバーは 1 つだけです。たとえば、GCC を使用しても、次の動作は未定義です。

union {
    double d;
    float f[2];
};
f[0] = 3.0f;
f[1] = 5.0f;
sum(d, f, 2); // UB: attempt to treat two members of
              // a union as simultaneously active

回避策

あるオブジェクトのビットを別の型のオブジェクトのビットとして再解釈する唯一の標準的な方法は、 と同等のものを使用することですmemcpy。これにより、オブジェクトのエイリアシングのための特別な規定が利用され、事実上、基になるオブジェクト表現をバイト レベルでchar読み取ったり変更したりできるようになります。たとえば、以下は正当であり、厳密なエイリアシング ルールに違反していません。

int a[2];
double d;
static_assert(sizeof(a) == sizeof(d));
memcpy(a, &d, sizeof(d));

これは、次のコードと意味的に同等です。

int a[2];
double d;
static_assert(sizeof(a) == sizeof(d));
for(size_t i = 0; i < sizeof(a); ++i)
   ((char*)a)[i] = ((char*)&d)[i];

GCC は、非アクティブな共用体メンバーからの読み取りを規定し、暗黙的にそれをアクティブにします。GCC のドキュメントから:

最後に書き込まれたものとは異なるユニオン メンバーから読み取る (「タイプ パニング」と呼ばれる) 慣行は一般的です。-fstrict-aliasing を使用しても、union 型を介してメモリにアクセスする場合は、型パニングが許可されます。したがって、上記のコードは期待どおりに機能します。構造体共用体列挙とビットフィールドの実装を参照してください。ただし、このコードでは次のことができない場合があります。

int f() {
    union a_union t;
    int* ip;
    t.d = 3.0;
    ip = &t.i;
    return *ip;
}

同様に、アドレスを取得し、結果のポインターをキャストし、結果を逆参照することによるアクセスには、キャストがユニオン型を使用している場合でも、未定義の動作があります。

int f() {
    double d = 3.0;
    return ((union a_union *) &d)->i;
} 

新しいプレースメント

(注:現在、標準にアクセスできないため、ここでは記憶で行っています)。オブジェクトをストレージ バッファーに新規配置すると、基になるストレージ オブジェクトの有効期間が暗黙的に終了します。これは、共用体のメンバーに書き込む場合と似ています。

union {
    int i;
    float f;
} u;

// No member of u is active. Neither i nor f refer to an lvalue of any type.
u.i = 5;
// The member u.i is now active, and there exists an lvalue (object)
// of type int with the value 5. No float object exists.
u.f = 5.0f;
// The member u.i is no longer active,
// as its lifetime has ended with the assignment.
// The member u.f is now active, and there exists an lvalue (object)
// of type float with the value 5.0f. No int object exists.

それでは、placement-new で同様のものを見てみましょう。

#define MAX_(x, y) ((x) > (y) ? (x) : (y))
// new returns suitably aligned memory
char* buffer = new char[MAX_(sizeof(int), sizeof(float))];
// Currently, only char objects exist in the buffer.
new (buffer) int(5);
// An object of type int has been constructed in the memory pointed to by buffer,
// implicitly ending the lifetime of the underlying storage objects.
new (buffer) float(5.0f);
// An object of type int has been constructed in the memory pointed to by buffer,
// implicitly ending the lifetime of the int object that previously occupied the same memory.

この種の暗黙的な有効期間の終了は、明らかな理由から、単純なコンストラクターとデストラクターを持つ型でのみ発生します。

于 2012-04-02T10:16:12.663 に答える
6

の場合のエラーは別として、 が原因sizeof(T) > sizeof(U)で、共用体が よりも適切で、場合によってはより高いアラインメントを持っているという問題が発生する可能性がありUますT。この共用体をインスタンス化せず、そのメモリ ブロックがアラインされるように (そして十分な大きさになるように!)、次に destination type のメンバーをフェッチするとT、最悪の場合、暗黙のうちに壊れます。

たとえば、4 バイトのアラインメントが必要な からU*、8バイトのアラインメントが必要な への C スタイルのキャストを行うと、アラインメント エラーが発生します。その後、8 バイトではなく 4 バイトにアラインされたタイプの共用体メンバーを読み取る可能性があります。Udummy_union*dummy_unionalignof(T) == 8T


エイリアス キャスト (位置合わせとサイズ セーフな reinterpret_cast は POD のみ):

この提案は厳密なエイリアシングに明示的に違反していますが、静的なアサーションがあります。

///@brief Compile time checked reinterpret_cast where destAlign <= srcAlign && destSize <= srcSize
template<typename _TargetPtrType, typename _ArgType>
inline _TargetPtrType alias_cast(_ArgType* const ptr)
{
    //assert argument alignment at runtime in debug builds
    assert(uintptr_t(ptr) % alignof(_ArgType) == 0);

    typedef typename std::tr1::remove_pointer<_TargetPtrType>::type target_type;
    static_assert(std::tr1::is_pointer<_TargetPtrType>::value && std::tr1::is_pod<target_type>::value, "Target type must be a pointer to POD");
    static_assert(std::tr1::is_pod<_ArgType>::value, "Argument must point to POD");
    static_assert(std::tr1::is_const<_ArgType>::value ? std::tr1::is_const<target_type>::value : true, "const argument must be cast to const target type");
    static_assert(alignof(_ArgType) % alignof(target_type) == 0, "Target alignment must be <= source alignment");
    static_assert(sizeof(_ArgType) >= sizeof(target_type), "Target size must be <= source size");

    //reinterpret cast doesn't remove a const qualifier either
    return reinterpret_cast<_TargetPtrType>(ptr);
}

ポインター型引数での使用 ( reinterpret_cast などの標準キャスト演算子と同様):

int* x = alias_cast<int*>(any_ptr);

別のアプローチ (一時的な共用体を使用してアライメントとエイリアシングの問題を回避します):

template<typename ReturnType, typename ArgType>
inline ReturnType alias_value(const ArgType& x)
{
    //test argument alignment at runtime in debug builds
    assert(uintptr_t(&x) % alignof(ArgType) == 0);

    static_assert(!std::tr1::is_pointer<ReturnType>::value ? !std::tr1::is_const<ReturnType>::value : true, "Target type can't be a const value type");
    static_assert(std::tr1::is_pod<ReturnType>::value, "Target type must be POD");
    static_assert(std::tr1::is_pod<ArgType>::value, "Argument must be of POD type");

    //assure, that we don't read garbage
    static_assert(sizeof(ReturnType) <= sizeof(ArgType),"Target size must be <= argument size");

    union dummy_union
    {
        ArgType x;
        ReturnType r;
    };

    dummy_union dummy;
    dummy.x = x;

    return dummy.r;
}

使用法:

struct characters
{
    char c[5];
};

//.....

characters chars;

chars.c[0] = 'a';
chars.c[1] = 'b';
chars.c[2] = 'c';
chars.c[3] = 'd';
chars.c[4] = '\0';

int r = alias_value<int>(chars);

これの欠点は、ユニオンが ReturnType に実際に必要なよりも多くのメモリを必要とする可能性があることです。


ラップされた memcpy (memcpy を使用して位置合わせとエイリアシングの問題を回避します):

template<typename ReturnType, typename ArgType>
inline ReturnType alias_value(const ArgType& x)
{
    //assert argument alignment at runtime in debug builds
    assert(uintptr_t(&x) % alignof(ArgType) == 0);

    static_assert(!std::tr1::is_pointer<ReturnType>::value ? !std::tr1::is_const<ReturnType>::value : true, "Target type can't be a const value type");
    static_assert(std::tr1::is_pod<ReturnType>::value, "Target type must be POD");
    static_assert(std::tr1::is_pod<ArgType>::value, "Argument must be of POD type");

    //assure, that we don't read garbage
    static_assert(sizeof(ReturnType) <= sizeof(ArgType),"Target size must be <= argument size");

    ReturnType r;
    memcpy(&r,&x,sizeof(ReturnType));

    return r;
}

POD タイプの動的サイズの配列の場合:

template<typename ReturnType, typename ElementType>
ReturnType alias_value(const ElementType* const array,const size_t size)
{
    //assert argument alignment at runtime in debug builds
    assert(uintptr_t(array) % alignof(ElementType) == 0);

    static const size_t min_element_count = (sizeof(ReturnType) / sizeof(ElementType)) + (sizeof(ReturnType) % sizeof(ElementType) != 0 ? 1 : 0);

    static_assert(!std::tr1::is_pointer<ReturnType>::value ? !std::tr1::is_const<ReturnType>::value : true, "Target type can't be a const value type");
    static_assert(std::tr1::is_pod<ReturnType>::value, "Target type must be POD");
    static_assert(std::tr1::is_pod<ElementType>::value, "Array elements must be of POD type");

    //check for minimum element count in array
    if(size < min_element_count)
        throw std::invalid_argument("insufficient array size");

    ReturnType r;
    memcpy(&r,array,sizeof(ReturnType));
    return r;
}

より効率的なアプローチでは、プリミティブを抽出するために、SSE からのものなどの組み込み関数を使用して明示的なアライメントされていない読み取りを行うことができます。


例:

struct sample_struct
{
    char c[4];
    int _aligner;
};

int test(void)
{
    const sample_struct constPOD    = {};
    sample_struct pod               = {};
    const char* str                 = "abcd";

    const int* constIntPtr  = alias_cast<const int*>(&constPOD);
    void* voidPtr           = alias_value<void*>(pod);
    int intValue            = alias_value<int>(str,strlen(str));

    return 0;
}

編集:

  • POD のみの変換を保証するアサーションが改善される可能性があります。
  • 不要なテンプレート ヘルパーを削除し、tr1 トレイトのみを使用するようになりました
  • const 値 (非ポインター) 戻り型の明確化と禁止のための静的アサーション
  • デバッグ ビルドのランタイム アサーション
  • 一部の関数引数に const 修飾子を追加
  • memcpy を使用した別のタイプのパニング関数
  • リファクタリング
  • 小さな例
于 2012-04-01T16:48:14.710 に答える
4

最も基本的なレベルでは、これは不可能であり、厳密なエイリアシングに違反していると思います。あなたが達成した唯一のことは、コンパイラをだまして気付かないようにすることです。

于 2012-04-01T16:20:16.147 に答える
2

私の質問は、念のために言っておきますが、このプログラムは標準に従って合法ですか?

いいえ。あなたが提供したエイリアスを使用すると、アライメントが不自然になる可能性があります。あなたが書いた共用体は、エイリアスのポイントを移動するだけです。動作しているように見えても、CPU オプション、ABI、またはコンパイラの設定が変更されると、そのプログラムが失敗する可能性があります。

そうでない場合、これを合法的に変更するにはどうすればよいでしょうか?

自然な一時変数を作成し、ストレージをメモリ ブロブとして扱う (ブロブから一時変数への出入り) か、すべてのタイプを表すユニオンを使用します (一度に 1 つのアクティブな要素であることを思い出してください)。

于 2012-04-01T17:08:20.633 に答える