web-dev-qa-db-ja.com

TridallyCopyableではないオブジェクトのstd :: memcpyの動作が未定義になるのはなぜですか?

http://en.cppreference.com/w/cpp/string/byte/memcpy から:

オブジェクトが TriviallyCopyable (スカラー、配列、C互換構造体など)でない場合、動作は未定義です。

私の仕事では、std::memcpyを使用して、次を使用してTriviallyCopyableではないオブジェクトをビット単位でスワップします。

void swapMemory(Entity* ePtr1, Entity* ePtr2)
{
   static const int size = sizeof(Entity); 
   char swapBuffer[size];

   memcpy(swapBuffer, ePtr1, size);
   memcpy(ePtr1, ePtr2, size);
   memcpy(ePtr2, swapBuffer, size);
}

問題はなかった。

悪用するのは簡単なことだと理解していますstd::memcpy TriviallyCopyableオブジェクトではないため、ダウンストリームで未定義の動作が発生します。しかし、私の質問:

なぜstd::memcpyそれ自体は、非TriviallyCopyableオブジェクトで使用される場合、未定義になりますか?なぜ規格はそれを指定する必要があると考えているのですか?

[〜#〜] update [〜#〜]

http://en.cppreference.com/w/cpp/string/byte/memcpy の内容は、この投稿と投稿への回答に応じて変更されています。現在の説明によると:

オブジェクトが TriviallyCopyable (スカラー、配列、C互換の構造体など)でない場合、プログラムがターゲットオブジェクトのデストラクタ(実行されない)の効果に依存しない限り、動作は未定義ですmemcpy)によって、ターゲットオブジェクトのライフタイム(終了しますが、memcpyによって開始されません)は、placement-newなどの他の手段によって開始されます。

[〜#〜] ps [〜#〜]

@Cubbiによるコメント:

@RSahu何かがダウンストリームのUBを保証する場合、プログラム全体が未定義になります。しかし、この場合はUBをすり抜けてcppreferenceを変更することが可能であると思われることに同意します。

67
R Sahu

なぜstd::memcpyそれ自体は、非TriviallyCopyableオブジェクトで使用される場合、未定義になりますか?

そうではありません!ただし、簡単にコピーできないタイプの1つのオブジェクトの基礎となるバイトをそのタイプの別のオブジェクトにコピーすると、ターゲットオブジェクトは生存していませんになります。ストレージを再利用することで破棄しましたが、コンストラクターの呼び出しで再起動していません。

ターゲットオブジェクトの使用(そのメンバー関数の呼び出し、データメンバーへのアクセス)は明らかに未定義です[basic.life]/6、およびそれに続く、暗黙のデストラクター呼び出しも同様です[basic.life]/4 自動保存期間を持つターゲットオブジェクトの場合。 未定義の動作は遡及的に注意してください。 [intro.execution]/5:

ただし、そのような実行に未定義の操作が含まれる場合、この国際標準は、その入力でそのプログラムを実行する実装に要件を課しません(最初の未定義の操作に先行する操作に関しても)。

実装が、オブジェクトがどのようにデッドであり、必然的に未定義のさらなる操作の対象となるかを見つけた場合、...プログラムのセマンティクスを変更することによって反応する可能性があります。 memcpy呼び出し以降。そして、オプティマイザーとそれらが行う特定の仮定を考えると、この考慮事項は非常に実用的になります。

ただし、標準ライブラリは、簡単にコピー可能な型に対して特定の標準ライブラリアルゴリズムを最適化することができ、許可されることに注意してください。 std::copy簡単にコピー可能な型へのポインタでは、通常、基礎となるバイトでmemcpyを呼び出します。 swapも同様です。
そのため、通常の一般的なアルゴリズムを使用して、コンパイラに適切な低レベルの最適化を実行させるだけです。 。また、これは、言語の矛盾した部分や不十分な部分を心配することで、脳を傷つけることを防ぎます。

38
Columbo

memcpyベースのswapが壊れるクラスを構築するのは簡単です:

struct X {
    int x;
    int* px; // invariant: always points to x
    X() : x(), px(&x) {}
    X(X const& b) : x(b.x), px(&x) {}
    X& operator=(X const& b) { x = b.x; return *this; }
};

そのようなオブジェクトをmemcpyingすると、その不変式が壊れます。

GNU C++ 11 std::stringは、短い文字列でまさにそれを行います。

これは、標準のファイルストリームと文字列ストリームの実装方法に似ています。ストリームは、最終的にstd::basic_iosへのポインターを含むstd::basic_streambufから派生します。ストリームには、std::basic_ios内のそのポインターが指す特定のバッファーもメンバー(または基本クラスのサブオブジェクト)として含まれています。

23

規格がそう言っているからです。

コンパイラは、非TriviallyCopyable型は、コピー/移動コンストラクタ/割り当て演算子を介してのみコピーされると想定する場合があります。これは最適化を目的とする場合があります(一部のデータがプライベートの場合、コピー/移動が発生するまで設定を延期できます)。

コンパイラはmemcpy呼び出しを自由に実行して、それを何もしないにするか、ハードドライブをフォーマットします。どうして?規格がそう言っているからです。そして、何もしないことはビットを移動するよりも確実に速いので、memcpyを同等に有効な高速プログラムに最適化してみませんか?

さて、実際には、それを予期しない型のビットをただ囲んでいるときに発生する可能性のある多くの問題があります。仮想関数テーブルが正しく設定されていない可能性があります。漏れを検出するために使用される計測器が正しく設定されていない可能性があります。 IDに位置が含まれるオブジェクトは、コードによって完全に台無しになります。

本当におもしろいのは、using std::swap; swap(*ePtr1, *ePtr2);を、コンパイラーが簡単にコピーできる型のためにmemcpyにコンパイルし、他の型の動作を定義できることです。コンパイラが、コピーがほんの一部コピーされていることを証明できる場合、それをmemcpyに自由に変更できます。そして、より最適なswapを書くことができれば、問題のオブジェクトの名前空間でそれを行うことができます。

C++は、オブジェクトがストレージの連続したバイトを占有することをすべてのタイプに対して保証するわけではありません[intro.object]/5

簡単にコピー可能なオブジェクトまたは標準レイアウトタイプ(3.9)のオブジェクトは、連続したストレージバイトを占有するものとします。

実際、仮想ベースクラスを通じて、主要な実装で不連続なオブジェクトを作成できます。オブジェクトの基本クラスのサブオブジェクトxxの開始アドレスの前にある例を構築しようとしました 。これを視覚化するには、次のグラフ/テーブルを検討してください。水平軸はアドレス空間で、垂直軸は継承のレベルです(レベル1はレベル0から継承します)。 dmでマークされたフィールドは、クラスのdirectデータメンバーによって占有されます。

 L | 00 08 16 
-+ --------- 
 1 | dm 
 0 | dm 

これは、継承を使用する場合の通常のメモリレイアウトです。ただし、仮想基本クラスサブオブジェクトの場所は固定されていません。これは、同じ基本クラスから仮想的に継承する子クラスによって再配置できるためです。これにより、レベル1(基本クラスサブ)オブジェクトが、アドレス8から始まり、16バイトのサイズであると報告する状況が発生する可能性があります。これらの2つの数値を単純に追加すると、実際に[0、16)を占有していても、アドレススペース[8、24)を占有すると考えられます。

そのようなレベル1オブジェクトを作成できる場合、memcpyを使用してそれをコピーすることはできません。memcpyは、このオブジェクトに属さないメモリ(アドレス16〜24)にアクセスします。私のデモでは、clang ++のアドレスサニタイザーによってstack-buffer-overflowとしてキャッチされます。

そのようなオブジェクトを構築する方法は?複数の仮想継承を使用することで、次のメモリレイアウトを持つオブジェクトを思い付きました(仮想テーブルポインターはvpとしてマークされています)。継承の4つの層で構成されます。

 L 00 08 16 24 32 40 48 
 3 dm 
 2 vp dm 
 1 vp dm 
 0 dm 

上記の問題は、レベル1の基本クラスサブオブジェクトで発生します。開始アドレスは32で、24バイトの大きさです(vptr、独自のデータメンバー、およびレベル0のデータメンバー)。

以下に、clang ++およびg ++ @ coliruでのこのようなメモリレイアウトのコードを示します。

struct l0 {
    std::int64_t dummy;
};

struct l1 : virtual l0 {
    std::int64_t dummy;
};

struct l2 : virtual l0, virtual l1 {
    std::int64_t dummy;
};

struct l3 : l2, virtual l1 {
    std::int64_t dummy;
};

次のようにstack-buffer-overflowを生成できます。

l3  o;
l1& so = o;

l1 t;
std::memcpy(&t, &so, sizeof(t));

メモリレイアウトに関する情報も出力する完全なデモを次に示します。

#include <cstdint>
#include <cstring>
#include <iomanip>
#include <iostream>

#define PRINT_LOCATION() \
    std::cout << std::setw(22) << __PRETTY_FUNCTION__                   \
      << " at offset " << std::setw(2)                                  \
        << (reinterpret_cast<char const*>(this) - addr)                 \
      << " ; data is at offset " << std::setw(2)                        \
        << (reinterpret_cast<char const*>(&dummy) - addr)               \
      << " ; naively to offset "                                        \
        << (reinterpret_cast<char const*>(this) - addr + sizeof(*this)) \
      << "\n"

struct l0 {
    std::int64_t dummy;

    void report(char const* addr) { PRINT_LOCATION(); }
};

struct l1 : virtual l0 {
    std::int64_t dummy;

    void report(char const* addr) { PRINT_LOCATION(); l0::report(addr); }
};

struct l2 : virtual l0, virtual l1 {
    std::int64_t dummy;

    void report(char const* addr) { PRINT_LOCATION(); l1::report(addr); }
};

struct l3 : l2, virtual l1 {
    std::int64_t dummy;

    void report(char const* addr) { PRINT_LOCATION(); l2::report(addr); }
};

void print_range(void const* b, std::size_t sz)
{
    std::cout << "[" << (void const*)b << ", "
              << (void*)(reinterpret_cast<char const*>(b) + sz) << ")";
}

void my_memcpy(void* dst, void const* src, std::size_t sz)
{
    std::cout << "copying from ";
    print_range(src, sz);
    std::cout << " to ";
    print_range(dst, sz);
    std::cout << "\n";
}

int main()
{
    l3 o{};
    o.report(reinterpret_cast<char const*>(&o));

    std::cout << "the complete object occupies ";
    print_range(&o, sizeof(o));
    std::cout << "\n";

    l1& so = o;
    l1 t;
    my_memcpy(&t, &so, sizeof(t));
}

ライブデモ

サンプル出力(垂直スクロールを回避するために短縮):

l3 :: reportオフセット0;データはオフセット16にあります。単純に相殺する 48
 l2 :: reportオフセット0;データはオフセット8にあります。単純に40 
 l1 :: reportをオフセット32でオフセットします。データはオフセット40にあります。単純に相殺する 56
 l0 ::オフセット24のレポート。データはオフセット24にあります。単純に32 
をオフセットするには、完全なオブジェクトが[0x9f0、0xa20)
 [0xa10、0xa28)から[0xa20、0xa38)
へのコピーを占有します

2つの強調された終了オフセットに注意してください。

15
dyp

これらの回答の多くは、memcpyがクラス内の不変式を壊す可能性があり、後で未定義の動作を引き起こす可能性があります(ほとんどの場合、リスクを冒さない十分な理由になるはずです)が、そうではないようですあなたは本当に求めています。

memcpy呼び出し自体が未定義の動作であると見なされる理由の1つは、ターゲットプラットフォームに基づいて最適化を行うためにコンパイラにできるだけ多くの余地を与えることです。呼び出し自体をUBにすることにより、コンパイラは許可になり、プラットフォームに依存した奇妙なことを行います。

この(非常に不自然で架空の)例について考えてみましょう。特定のハードウェアプラットフォームでは、いくつかの異なる種類のメモリが存在する場合があります。たとえば、余分な高速メモリコピーを許可する特殊なメモリがあります。したがって、この(架空の)プラットフォームのコンパイラは、すべてのTriviallyCopyable型をこの特別なメモリに配置し、memcpyを実装して、このメモリでのみ動作する特別なハードウェア命令を使用できます。

このプラットフォームのmemcpy以外のオブジェクトでTriviallyCopyableを使用すると、低レベルの無効なOPCODEクラッシュが発生する可能性がありますmemcpy呼び出し自体で

おそらく最も説得力のある引数ではありませんが、ポイントは、標準の禁止しないです。これは、memcpycallを作成することによってのみ可能です。 UB。

5
CAdaker

memcpyはすべてのバイトをコピーします。または、場合によってはすべてのバイトをスワップします。熱心なコンパイラーは、「未定義の振る舞い」をあらゆる種類のいたずらの言い訳としてとることができますが、ほとんどのコンパイラーはそうしません。それでも、それは可能です。

ただし、これらのバイトがコピーされた後、それらをコピーしたオブジェクトは有効なオブジェクトではなくなる可能性があります。シンプルなケースは、大きな文字列がメモリを割り当てる文字列実装ですが、小さな文字列は文字列オブジェクトの一部を使用して文字を保持し、そのポインタを保持します。ポインタは明らかに他のオブジェクトを指しているので、物事は間違っています。私が見た別の例は、ごく少数のインスタンスでのみ使用されるデータを持つクラスでした。そのため、データはオブジェクトのアドレスをキーとしてデータベースに保持されていました。

たとえば、インスタンスにミューテックスが含まれている場合、それを移動することは大きな問題になると思います。

3
gnasher729

まず、変更可能なC/C++オブジェクトのすべてのメモリが、型指定されていない、特殊化されていない、すべての変更可能なオブジェクトで使用可能でなければならないことは疑いのないことに注意してください。 (グローバルconst変数のメモリは仮想的に型付けできると思いますが、このような小さなコーナーケースの場合、このようなハイパーコンプリケーションを使用しても意味がありません。)Javaとは異なり、C++には型指定された動的オブジェクトの割り当てはありませんnew Class(args) in Javaは型付きオブジェクトの作成です。型付きメモリに存在する可能性のある、明確に定義された型のオブジェクトを作成します。一方、C++式new Class(args)は、タイプなしのメモリ割り当てに対する薄い型付けラッパーであり、new (operator new(sizeof(Class)) Class(args)と同等です:オブジェクトは「ニュートラルメモリ」に作成されます。これを変更すると、C++の非常に大きな部分が変更されます。

あるタイプでビットコピー操作(memcpyまたは同等のユーザー定義のバイトごとのコピー)を禁止すると、ポリモーフィッククラス(仮想関数を使用するクラス)の実装に多くの自由が与えられます。 「仮想クラス」(標準用語ではない)、つまりvirtualキーワードを使用するクラスです。

ポリモーフィッククラスの実装では、ポリモーフィックオブジェクトとその仮想関数のアドレスを関連付けるアドレスのグローバルな連想マップを使用できます。これは、C++言語の最初の反復(または「クラス付きC」)の設計中に真剣に検討されたオプションだと思います。ポリモーフィックオブジェクトのマップは、特別なCPU機能と特別な連想メモリを使用する場合があります(このような機能はC++ユーザーには公開されません)。

もちろん、仮想関数のすべての実用的な実装は、vtables(クラスのすべての動的な側面を記述する定数レコード)を使用し、各ポリモーフィックベースクラスサブオブジェクトにvptr(vtableポインター)を置くことを知っています。少なくとも最も単純なケースでは)非常に効率的です。デバッグモード以外の現実世界の実装には、多態的なオブジェクトのグローバルレジストリはありません(このようなデバッグモードはわかりません)。

C++標準では、オブジェクトのメモリを再利用する場合はデストラクタ呼び出しをスキップできると述べて、グローバルレジストリをやや公式の欠如にしています。そのデストラクタ呼び出しの「副作用」に依存しません。 (つまり、「副作用」はユーザーが作成したものであり、実装によってデストラクタに自動的に行われるように、作成された実装ではなくデストラクタの本体です。)

実際には、すべての実装で、コンパイラはvptr(vtablesへのポインター)隠しメンバーを使用するだけであり、これらの隠しメンバーはby memcpyによって適切にコピーされるためです。ポリモーフィッククラス(すべての非表示メンバー)を表すC構造体の単純なメンバーごとのコピーを実行したかのように。ビット単位のコピー、または完全なC構造体のメンバー単位のコピー(完全なC構造体には非表示のメンバーが含まれます)は、コンストラクター呼び出しとまったく同じように動作します(新しい配置によって行われます)。プレースメントを新規と呼びました。強力な外部関数呼び出し(インライン化できない関数の呼び出し、および動的にロードされたコードユニットで定義された関数の呼び出し、またはシステムコールのように、コンパイラによって実装を確認できない関数)を実行する場合、コンパイラーは、そのようなコンストラクターが検査できないコードによって呼び出された可能性があると想定します。 したがって、ここのmemcpyの動作は、言語標準ではなく、コンパイラABI(Application Binary Interface)によって定義されます。動作強力な外部関数呼び出しは、言語標準だけでなく、ABIによって定義されます。潜在的にインライン化できない関数の呼び出しは、その定義がわかるように言語によって定義されます(コンパイラー中またはリンク時のグローバル最適化中)。

したがって、実際には、適切な「コンパイラフェンス」(外部関数の呼び出し、または単にasm(""))が与えられると、仮想関数のみを使用するmemcpyクラスを使用できます。

もちろん、memcpyを実行するときに新しい言語のセマンティクスによってそのような配置を許可する必要があります。既存のオブジェクトの動的な型を自由に再定義し、単に古いものを破壊したふりをすることはできませんオブジェクト。非constグローバル、静的、自動、メンバーサブオブジェクト、配列サブオブジェクトがある場合、それを上書きして、別の無関係なオブジェクトをそこに置くことができます。ただし、動的タイプが異なる場合、それが同じオブジェクトまたはサブオブジェクトであると偽ることはできません。

struct A { virtual void f(); };
struct B : A { };

void test() {
  A a;
  if (sizeof(A) != sizeof(B)) return;
  new (&a) B; // OK (assuming alignement is OK)
  a.f(); // undefined
}

既存のオブジェクトの多相型の変更は単に許可されません。新しいオブジェクトは、メモリ領域(&aで始まる連続したバイト)を除き、aとは関係がありません。彼らはさまざまな種類があります。

[標準は、*&aを使用して(通常のフラットメモリマシンで)または(A&)(char&)a(いずれにしても)を使用して新しいオブジェクトを参照できるかどうかで大きく分けられます。コンパイラの作成者は分割されていません。実行しないでください。これはC++の重大な欠陥であり、おそらく最も深く厄介な問題です。]

しかし、移植可能なコードでは、仮想継承を使用するクラスのビットごとのコピーを実行できません。一部の実装では、仮想ベースサブオブジェクトへのポインターを使用してこれらのクラスを実装します。これらのポインターは、最も派生したオブジェクトのコンストラクターmemcpy(すべての隠されたメンバーを持つクラスを表すC構造体の単純なメンバーごとのコピーのように)派生オブジェクトのサブオブジェクトをポイントしません!

他のABIは、アドレスオフセットを使用してこれらのベースサブオブジェクトを見つけます。最終的なオーバーライドやtypeidなど、最も派生したオブジェクトのタイプのみに依存するため、vtableに格納できます。これらの実装では、memcpyはABIによって保証されたとおりに動作します(既存のオブジェクトのタイプを変更する場合の上記の制限付き)。

どちらの場合でも、それは完全にオブジェクト表現の問題、つまりABIの問題です。

1
curiousguy

memcpyがUBであるもう1つの理由は(他の回答で言及されていることとは別に-後に不変式を破ることがあります)、標準が正確に言うのが非常に難しいことです何が起こるか

非自明な型の場合、標準では、オブジェクトがメモリにどのように配置されるか、メンバーが配置される順序、vtableポインターがどこにあるか、パディングが何であるかなどについてほとんど言及されていません。これを決定する際に。

その結果、標準がこれらの「安全な」状況でmemcpyを許可したい場合でも、どの状況が安全であり、そうでないか、または実際のUBがいつトリガーされるかを述べることは不可能です。安全でないケース。

効果は実装定義または不特定であるべきだと主張することができると思いますが、個人的にはプラットフォームの詳細を少し掘り下げて、一般的な場合に正当性を少し与えすぎていると感じますむしろ安全ではありません。

1
CAdaker

ここで私が理解できるのは、一部の実用的なアプリケーションでは、C++標準mayが制限的であり、むしろ許容性が十分でないことです。

他の回答に示されているように、memcpyは「複雑な」タイプではすぐに壊れますが、私見では、実際にshouldmemcpyがない限り、標準レイアウトタイプで機能します定義されたコピー操作と標準レイアウトタイプのデストラクタの機能を破壊します。 (偶数TCクラスはallowedであり、重要なコンストラクターを持っていることに注意してください。)標準はTCタイプwrtのみを明示的に呼び出します。ただし、これ。

最近の見積案(N3797):

3.9タイプ

...

2簡単にコピー可能なタイプTのオブジェクト(ベースクラスサブオブジェクト以外)の場合、オブジェクトがタイプTの有効な値を保持しているかどうか、オブジェクトを構成する基本バイト(1.7)は、charまたはunsigned charの配列にコピーできます。 charまたはunsigned charの配列の内容がオブジェクトにコピーされた場合、オブジェクトは元の値を保持します。 [例:

  #define N sizeof(T)
  char buf[N];        T obj; // obj initialized to its original value
  std::memcpy(buf, &obj, N); // between these two calls to std::memcpy,       
                             // obj might be modified         
  std::memcpy(&obj, buf, N); // at this point, each subobject of obj of scalar type
                             // holds its original value 

—例の終了]

3Tへの2つのポインターが別個のTオブジェクトobj1とobj2を指している場合、obj1もobj2もベースクラスではない場合サブオブジェクト。obj1を構成する基本バイト(1.7)がobj2にコピーされる場合、obj2はその後obj1と同じ値を保持します。 [例:

T* t1p;
T* t2p;       
     // provided that t2p points to an initialized object ...         
std::memcpy(t1p, t2p, sizeof(T));  
     // at this point, every subobject of trivially copyable type in *t1p contains        
     // the same value as the corresponding subobject in *t2p

—例の終了]

ここでの標準は trivially copyable タイプについて説明していますが、上記の@dypによって 観測された として、 標準レイアウトタイプ もあります。これは、私が見る限り、Trivally Copyableタイプと必ずしも重複しません。

標準は言う:

1.8 C++オブジェクトモデル

(...)

5(...)簡単にコピー可能な、または標準レイアウトのタイプ(3.9)のオブジェクトは、連続したバイトのストレージを占有します。

私がここで見ているのはそれです:

  • 標準では、自明ではないコピー可能な型については何も述べていません。 memcpy。 (ここですでに何度か述べたように)
  • 標準には、連続したストレージを占有する標準レイアウトタイプ用の個別のコンセプトがあります。
  • 標準ではないは、標準レイアウトのオブジェクトでmemcpyの使用を明示的に許可または禁止しますnot Trivially Copyable 。

explicitly UBと呼ばれているようには見えませんが、確かに nspecified behavior と呼ばれるものでもありません。受け入れられた回答へのコメントで@underscore_dが行ったことを結論付けることができます。

(...)「まあ、それはUBとして明示的に呼び出されなかったので、定義された動作です!」と言うことはできません。これがこのスレッドに相当すると思われます。 N3797 3.9ポイント2〜3は、memcpyが自明ではないコピー可能なオブジェクトに対して行うことを定義しないため、(...)[t]それは信頼性の高い、つまり移植可能なコードを書くのに役に立たないので、私の目にはUBとほぼ機能的に同等です

I personallyは、移植性に関する限りUBになると結論付けます(ああ、それらのオプティマイザー)が、具体的な実装についてのいくらかのヘッジと知識があれば、それでうまくいくと思います。 (トラブルに見合うだけの価値があることを確認してください。)


サイドノート:標準は本当にmemcpy混乱全体に標準レイアウトタイプのセマンティクスを明確に組み込むべきだと思います。なぜなら、それは非Trivially Copyableオブジェクトのビット単位のコピーを行うための有効で有用なユースケースだからです。ここに。

リンク: memcpyを使用して、隣接する複数の標準レイアウトサブオブジェクトに書き込むことはできますか?

0
Martin Ba