7

C++ を含む OOP 言語では、オブジェクト ポインターを生のバイナリ データへのポインターとして扱うことは想定されていません。オブジェクトは、その表現を「超える」ものです。

したがって、たとえば、swapバイトをスワップして 2 つのオブジェクトを ing することは正しくありません。

template<class T>
void bad_swap(T &a, T &b)  // Assuming T is the most-derived type of the object
{
    char temp[sizeof(T)];
    memcpy(temp, &a, sizeof(a));
    memcpy(&a, &b, sizeof(b));
    memcpy(&b, temp, sizeof(temp));
}

ただし、このショートカットが問題を引き起こしていると想像できる唯一の状況は、オブジェクトにそれ自体へのポインターが含まれている場合です。ただし、他のシナリオもあるかもしれません。

swapビットごとのスワップを実行した場合に正しいものが壊れる実際の (現実の) 例は何ですか?
自己ポインターを使った不自然な例は簡単に思いつくことができますが、実際の例は思いつきません。

4

5 に答える 5

13

これは具体的なことではありませんswapが、低レベルの最適化は問題にならない可能性があることを示す例です。とにかく、コンパイラはしばしばそれを理解します。

もちろん、これはコンパイラが非常に幸運である私のお気に入りの例ですが、とにかく、コンパイラが愚かで、生成されたコードをいくつかの簡単なトリックで簡単に改善できると想定すべきではありません。

私のテストコードは - std::string を構築してコピーします。

std::string whatever = "abcdefgh";
std::string whatever2 = whatever;

最初のコンストラクタは次のようになります

  basic_string(const value_type* _String,
               const allocator_type& _Allocator = allocator_type() ) : _Parent(_Allocator)
  {
     const size_type _StringSize = traits_type::length(_String);

     if (_MySmallStringCapacity < _StringSize)
     {
        _AllocateAndCopy(_String, _StringSize);
     }
     else
     {
        traits_type::copy(_MySmallString._Buffer, _String, _StringSize);

        _SetSmallStringCapacity();
        _SetSize(_StringSize);
     }
  }

生成されたコードは

   std::string whatever = "abcdefgh";
000000013FCC30C3  mov         rdx,qword ptr [string "abcdefgh" (13FD07498h)]  
000000013FCC30CA  mov         qword ptr [whatever],rdx  
000000013FCC30D2  mov         byte ptr [rsp+347h],0  
000000013FCC30DA  mov         qword ptr [rsp+348h],8  
000000013FCC30E6  mov         byte ptr [rsp+338h],0  

これは、文字列全体の 1 つのレジスタ コピーに最適化されます (適合するように慎重に選択されます) traits_type::copymemcpyまた、コンパイラは への呼び出しをstrlenコンパイル時の に変換します8

次に、それを新しい文字列にコピーします。コピーコンストラクタは次のようになります

  basic_string(const basic_string& _String)
     : _Parent(std::allocator_traits<allocator_type>::select_on_container_copy_construction(_String._MyAllocator))
  {
     if (_MySmallStringCapacity < _String.size())
     {
        _AllocateAndCopy(_String);
     }
     else
     {
        traits_type::copy(_MySmallString._Buffer, _String.data(), _String.size());

        _SetSmallStringCapacity();
        _SetSize(_String.size());
     }
  }

たった 4 つの機械語命令になります。

   std::string whatever2 = whatever;
000000013FCC30EE  mov         qword ptr [whatever2],rdx  
000000013FCC30F6  mov         byte ptr [rsp+6CFh],0  
000000013FCC30FE  mov         qword ptr [rsp+6D0h],8  
000000013FCC310A  mov         byte ptr [rsp+6C0h],0  

オプティマイザは、charがまだ登録されrdxていること、および文字列の長さが同じでなければならないことを記憶していることに注意してください8

このようなものを見た後、私は自分のコンパイラを信頼し、少しいじってコードを改善しようとするのを避けたいと思っています。プロファイリングで予期しないボトルネックが見つからない限り、これは役に立ちません。

(MSVC 10 と私の std::string 実装をフィーチャー)

于 2012-07-24T21:05:56.413 に答える
8

プロファイリングが行われ、より明白で明確な実装にパフォーマンスの問題があるという特定のケースを除いて、これはほとんど常に悪い考えであると主張します。swapその場合でも、この種のアプローチは、継承のないまっすぐな構造に対してのみ使用し、どのような種類のクラスに対しても使用しません。継承がいつ追加されて全体が壊れる可能性があるかはわかりません (おそらく本当に陰湿な方法でも)。

高速なスワップ実装が必要な場合は、(適切な場合に) クラスを pimpl してから実装をスワップ アウトすることをお勧めします (繰り返しますが、これは所有者へのバックポインターがないことを前提としていますが、それはクラスに簡単に含まれます)。外的要因ではなく & 実装)。

編集:このアプローチで考えられる問題:

  • 自分自身へのポインタ (直接的または間接的)
  • クラスに、単純なバイトコピーが無意味である (事実上この定義を再帰する) オブジェクト、またはコピーが通常無効になっているオブジェクトが含まれている場合
  • クラスがコピーするために何らかの種類のロックを必要とする場合
  • ここで誤って 2 つの異なる型を渡してしまい (暗黙的に派生クラスを親のように見せるために必要なのは 1 つの中間関数だけです)、vptrs を交換することは簡単です (OUCH!)
于 2012-07-24T20:18:00.927 に答える
3

「自己ポインタ」はなぜ工夫されているのですか?

class RingBuffer
{
    // ...
private:
    char buffer[1024];
    char* curr;
};

この型は、バッファーとバッファー内の現在位置を保持します。

または、iostream について聞いたことがあるかもしれません。

class streambuf
{
  char buffer[64];
  char* put_ptr;
  char* get_ptr;
  // ...
};

他の誰かが述べたように、小さな文字列の最適化:

// untested, probably buggy!
class String {
  union {
    char buf[8];
    char* ptr;
  } data;
  unsigned len;
  unsigned capacity;
  char* str;
public:
  String(const char* s, unsigned n)
  {
    if (n > sizeof(data.buf)-1) {
      str = new char[n+1];
      len = capacity = n;
    }
    else
    {
      str = data.buf;
      len = n;
      capacity = sizeof(data.buf) - 1;
    } 
    memcpy(str, s, n);
    str[n] = '\0';
  }
  ~String()
  {
    if (str != data.buf)
      delete[] str;
  }
  const char* c_str() const { return str; }
  // ...
};

これにはセルフポインターもあります。2 つの小さな文字列を作成してからそれらを交換すると、デストラクタは文字列が「非ローカル」であると判断し、メモリを削除しようとします。

{
  String s1("foo", 3);
  String s2("bar", 3);
  bad_swap(s1, s2);
}  // BOOM! destructors delete stack memory

ヴァルグリンドは次のように述べています。

==30214== Memcheck, a memory error detector
==30214== Copyright (C) 2002-2010, and GNU GPL'd, by Julian Seward et al.
==30214== Using Valgrind-3.6.1 and LibVEX; rerun with -h for copyright info
==30214== Command: ./a.out
==30214== 
==30214== Invalid free() / delete / delete[]
==30214==    at 0x4A05E9C: operator delete[](void*) (vg_replace_malloc.c:409)
==30214==    by 0x40083F: String::~String() (in /dev/shm/a.out)
==30214==    by 0x400737: main (in /dev/shm/a.out)
==30214==  Address 0x7fefffd00 is on thread 1's stack
==30214== 
==30214== Invalid free() / delete / delete[]
==30214==    at 0x4A05E9C: operator delete[](void*) (vg_replace_malloc.c:409)
==30214==    by 0x40083F: String::~String() (in /dev/shm/a.out)
==30214==    by 0x400743: main (in /dev/shm/a.out)
==30214==  Address 0x7fefffce0 is on thread 1's stack

これは、 や のような型に影響を与えることを示してstd::streambufおりstd::string、ほとんど不自然で難解な例です。

基本的に、型が自明にコピー可能な場合、デフォルトbad_swapは最適です(コンパイラが memcpy に最適化せstd::swap、より良いコンパイラを取得します)。それらが自明にコピー可能でない場合、それは素晴らしいですUndefined Behavior 氏と彼の友人である深刻なバグ氏に会う方法。

于 2012-07-24T20:39:00.330 に答える
2

他の回答で言及されている例(特に、オブジェクト自体の一部へのポインターを含むオブジェクトとロックが必要なオブジェクト)に加えて、それに応じて更新する必要がある外部データ構造によって管理されているオブジェクトへのポインターの場合もあります(例は、過度にならないようにいくらか工夫されています (テストされていないため、バグがある可能性があります)):

class foo
{
private:
   static std::map<foo*, int> foo_data;
public:
   foo() { foo_data.emplace(this, 0); }
   foo(const foo& f) { foo_data.emplace(this, foo_data[&f]); }
   foo& operator=(const foo& f) { foo_data[this] = foo_data[&f]; return *this}
   ~foo() { foo_data.erase(this); }
   ...
};

オブジェクトが によって交換されると、明らかにこのようなものはひどく壊れますmemcpy。もちろん、これに関する実際の例は通常、もう少し複雑ですが、要点は明確です。

例に加えて、このような自明でないコピー可能オブジェクトのコピー (またはスワップ) は、標準では定義されていない動作だと思います (後で確認するかもしれません)。その場合、そのコードがより複雑なオブジェクトで機能するという保証はまったくありません。

于 2012-07-24T20:53:53.517 に答える
1

まだ言及されていないもの:

  • スワップには副作用がある場合があります。たとえば、外部要素のポインタを更新して新しい場所を指すようにするか、オブジェクトの内容が変更されたことをリッスンしているオブジェクトに通知する必要があります。
  • 相対アドレスを使用する 2 つの要素を交換すると問題が発生する
于 2012-07-24T20:40:24.887 に答える