web-dev-qa-db-ja.com

未定義の動作なしでdoubleにfloatバッファーを再利用する

ある特定のC++関数では、doubleの半分の数を格納するために一時的に使用したいfloatの大きなバッファーへのポインターがあります。このバッファをdoubleを格納するためのスクラッチスペースとして使用する方法はありますか?これは、標準で許可されています(つまり、未定義の動作ではありません)。

要約すると、私はこれが欲しいです:

void f(float* buffer)
{
  double* d = reinterpret_cast<double*>(buffer);
  // make use of d
  d[i] = 1.;
  // done using d as scratch, start filling the buffer
  buffer[j] = 1.;
}

私が見る限り、これを行う簡単な方法はありません。正しく理解していれば、reinterpret_cast<double*>このように型のエイリアシングが原因で未定義の動作が発生し、memcpyまたはfloat/double unionを使用するには、データをコピーして余分なスペースを割り当てる必要があります。これにより、目的が損なわれ、私の場合はコストがかかります(C++では型のパンニングにユニオンを使用することは許可されていません)。

フロートバッファは、doubleに使用するために正しく調整されていると見なすことができます。

25
André Offringa

次のコードはそれを行うための有効な方法だと思います(これは実際にはアイデアに関する小さな例にすぎません)。

#include <memory>

void f(float* buffer, std::size_t buffer_size_in_bytes)
{
    double* d = new (buffer)double[buffer_size_in_bytes / sizeof(double)];

    // we have started the lifetime of the doubles.
    // "d" is a new pointer pointing to the first double object in the array.        
    // now you can use "d" as a double buffer for your calculations
    // you are not allowed to access any object through the "buffer" pointer anymore since the floats are "destroyed"       
    d[0] = 1.;
    // do some work here on/with the doubles...


    // conceptually we need to destory the doubles here... but they are trivially destructable

    // now we need to start the lifetime of the floats again
    new (buffer) float[10];  


    // here we are unsure about wether we need to update the "buffer" pointer to 
    // the one returned by the placement new of the floats
    // if it is nessessary, we could return the new float pointer or take the input pointer
    // by reference and update it directly in the function
}

int main()
{
    float* floats = new float[10];
    f(floats, sizeof(float) * 10);
    return 0;
}

Newの配置から受け取ったポインタのみを使用することが重要です。そして、フロートの後ろに新しいものを配置することが重要です。無操作構造であっても、フロートの寿命をやり直す必要があります。

のことを忘れます std::launderおよびreinterpret_castコメントで。新しい配置はあなたのために仕事をします。

編集:メインでバッファを作成するときは、適切に配置されていることを確認してください。

更新:

コメントで議論された事柄についての最新情報を提供したかっただけです。

  1. 最初に述べたのは、最初に作成されたfloatポインターを、再配置によって返されたポインターに更新する必要があるかもしれないということでした-新しくなったfloat(問題は、最初にfloatポインターを使用してfloatにアクセスできるかどうかです。フロートは、追加の新しい式によって取得された「新しい」フロートになりました)。

これを行うには、a)参照によってfloatポインターを渡して更新するか、b)関数から新しく取得したfloatポインターを返すことができます。

a)

void f(float*& buffer, std::size_t buffer_size_in_bytes)
{
    double* d = new (buffer)double[buffer_size_in_bytes / sizeof(double)];    
    // do some work here on/with the doubles...
    buffer = new (buffer) float[10];  
}

b)

float* f(float* buffer, std::size_t buffer_size_in_bytes)
{
    /* same as inital example... */
    return new (buffer) float[10];  
}

int main()
{
    float* floats = new float[10];
    floats = f(floats, sizeof(float) * 10);
    return 0;
}
  1. 次に言及するより重要なことは、placement-newはメモリオーバーヘッドを持つことが許可されているということです。したがって、実装では、返された配列の前にメタデータを配置できます。それが起こった場合、いくつのダブルが私たちの記憶に収まるかという素朴な計算は明らかに間違っています。問題は、実装が特定の呼び出しのために事前に取得するバイト数がわからないことです。ただし、残りのストレージに収まることがわかっているdoubleの量を調整する必要があります。ここ( https://stackoverflow.com/a/8721932/3783662 )は別のSO投稿で、ハワード・ヒナントがテストスニペットを提供しました。オンラインを使用してこれをテストしましたコンパイラーは、些細な破壊可能な型(doubleなど)の場合、オーバーヘッドが0であることを確認しました。より複雑な型(std :: stringなど)の場合、8バイトのオーバーヘッドがありました。ただし、これはプラットフォーム/コンパイラーによって異なる場合があります。ハワードのスニペットで事前にテストしてください。

  2. ある種の配置newを使用する必要がある理由(new []または単一要素newのいずれかによる)の質問の場合:ポインターを任意の方法でキャストできます。しかし、最終的には、値にアクセスするときに、厳密なエイリアシングルールの違反を避けるために、適切なタイプを使用する必要があります。簡単に言えば、ポインタで指定された場所にポインタタイプのオブジェクトが実際に存在する場合にのみ、オブジェクトへのアクセスが許可されます。では、どのようにしてオブジェクトに命を吹き込みますか?標準は言う:

https://timsong-cpp.github.io/cppwp/intro.object#1

「オブジェクトは、定義によって、新しい式によって、ユニオンのアクティブメンバーを暗黙的に変更するとき、または一時オブジェクトが作成されるときに作成されます。」

興味深いと思われる追加のセクターがあります。

https://timsong-cpp.github.io/cppwp/basic.life#1

「オブジェクトがクラスまたは集約タイプであり、オブジェクトまたはそのサブオブジェクトの1つが、些細なデフォルトコンストラクター以外のコンストラクターによって初期化される場合、オブジェクトは非空の初期化を持っていると言われます。タイプTのオブジェクトの存続期間は、次の場合に始まります。

  • タイプTの適切な配置とサイズのストレージが取得され、
  • オブジェクトに空でない初期化がある場合、その初期化は完了しています。」

さて、ダブルスは些細なことなので、些細なオブジェクトを生き生きとさせ、実際の生きているオブジェクトを変更するために何らかの行動を取る必要があると主張するかもしれません。フロート用のストレージを最初に取得し、ダブルポインターを介してストレージにアクセスすると、厳密なエイリアシングに違反するため、「はい」と言います。したがって、実際の型が変更されたことをコンパイラに通知する必要があります。この最後のポイント3全体は、かなり物議を醸した議論でした。あなたはあなた自身の意見を形成するかもしれません。これですべての情報が手元にあります。

10
phön

これは2つの方法で実現できます。

最初:

void set(float *buffer, size_t index, double value) {
    memcpy(reinterpret_cast<char*>(buffer)+sizeof(double)*index, &value, sizeof(double));
}
double get(const float *buffer, size_t index) {
    double v;
    memcpy(&v, reinterpret_cast<const char*>(buffer)+sizeof(double)*index, sizeof(double));
    return v;
}
void f(float *buffer) {
    // here, use set and get functions
}

2番目:float *の代わりに、「タイプレス」のchar[]バッファを割り当て、新しい配置を使用してフロートまたはダブルを内部に配置する必要があります。

template <typename T>
void setType(char *buffer, size_t size) {
    for (size_t i=0; i<size/sizeof(T); i++) {
        new(buffer+i*sizeof(T)) T;
    }
}
// use it like this: setType<float>(buffer, sizeOfBuffer);

次に、このアクセサーを使用します。

template <typename T>
T &get(char *buffer, size_t index) {
    return *std::launder(reinterpret_cast<T *>(buffer+index*sizeof(T)));
}
// use it like this: get<float>(buffer, index) = 33.3f;

3番目の方法はphönの答えのようなものかもしれません(その答えの下の私のコメントを参照してください)、残念ながら私は この問題 のために適切な解決策を作ることができません。

7
geza

これは、それほど怖くない別のアプローチです。

あなたは言う、

...フロート/ダブルユニオンは...余分なスペースを割り当てないと不可能です。これは目的を損ない、私の場合はコストがかかります...

したがって、各ユニオンオブジェクトに1つではなく2つのfloatを含めるだけです。

static_assert(sizeof(double) == sizeof(float)*2, "Assuming exactly two floats fit in a double.");
union double_or_floats
{
    double d;
    float f[2];
};

void f(double_or_floats* buffer)
{
    // Use buffer of doubles as scratch space.
    buffer[0].d = 1.0;
    // Done with the scratch space.  Start filling the buffer with floats.
    buffer[0].f[0] = 1.0f;
    buffer[0].f[1] = 2.0f;
}

もちろん、これによりインデックス作成がより複雑になり、呼び出しコードを変更する必要があります。しかし、オーバーヘッドはなく、より明らかに正しいです。

2
Maxpm

tl; drコマンドラインで実行することをコンパイラに指示しない限り、ポインタのエイリアスを作成しないでください。


これを行う最も簡単な方法は、どのコンパイラスイッチが厳密なエイリアシングを無効にしているかを把握し、問題のソースファイルに使用することです。

必要がありますね?


これについてもう少し考えました。新しい配置に関するすべてのことにもかかわらず、これが唯一の安全な方法です。

どうして?

同じアドレスを指す異なるタイプの2つのポインターがある場合は、そのアドレスにエイリアスを設定しているため、コンパイラーをだます可能性が高くなります。 そして、それらのポインタにどのように値を割り当てたかは関係ありません。コンパイラはそれを覚えていません。

したがって、これが唯一の安全な方法であり、それがstd::punが必要な理由です。

1
Paul Sanders

この問題は、ポータブルC++では解決できません。

ポインタのエイリアスに関しては、C++は厳密です。逆説的ですが、これにより、非常に多くのプラットフォームでコンパイルできます(たとえば、double番号がfloat番号とは異なる場所に格納されている場合など)。

言うまでもなく、移植可能なコードを求めているのであれば、持っているものを再コード化する必要があります。次善の策は、実用的であることです。これは、私が遭遇したすべてのデスクトップシステムで機能することを受け入れます。おそらくstatic_assertコンパイラ名/アーキテクチャについて。

0
Bathsheba