Примеры того, когда побитовая swap () - плохая идея?

вы не должны рассматривать указатели объектов как указатели на необработанные двоичные данные на языках ООП, включая C++. Объекты-это "больше, чем" их представление.

Так, например, swaping два объекта путем замены их байтов неверно:

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 сломался бы, если бы вы выполнили побитовый своп?
Я могу легко придумать надуманные примеры с указателями на себя, но я не могу придумать ни одного реального мира.

5 ответов


Я буду утверждать, что это почти всегда плохо идея за исключением конкретного случая, когда профилирование было сделано и более очевидная и ясная реализация swap имеет проблемы с производительностью. Даже в этом случае я бы пошел только с таким подходом для прямых структур без наследования, никогда для какого-либо класса. Вы никогда не знаете, когда наследство будет добавлено, потенциально нарушая все это (возможно, действительно коварными способами).

Если вы хотите быструю реализацию свопа, возможно, лучшим выбором (где это уместно) является сутенерство класса, а затем просто поменять реализацию (опять же, это предполагает, что нет обратных указателей на владельца, но это легко содержится в классе & impl, а не внешние факторы).

EDIT: возможные проблемы с этим подходом:

  • указатели назад к себе (прямо или косвенно)
  • если класс содержит какой-либо объект, для которого прямая байт-копия бессмысленна (по сути рекурсией это определение), или копирование обычно отключен
  • если классу нужна какая-либо блокировка для копирования
  • это легко чтобы случайно передать здесь два разных типа (все, что требуется, это одна промежуточная функция, чтобы неявно сделать производный класс похожим на родительский), а затем вы меняете vptrs (ой!)

это не конкретно про 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  

здесь traits_type::copyсодержит вызов memcpy, который оптимизирован в одну регистровую копию всей строки (тщательно подобранную по размеру). Компилятор преобразует вызов 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 и my std:: string реализация)


почему" само-указатели " придуманы?

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

этот тип содержит буфер и текущую позицию в буфер.

или, может быть, вы слышали об iostreams:

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] = '';
  }
  ~String()
  {
    if (str != data.buf)
      delete[] str;
  }
  const char* c_str() const { return str; }
  // ...
};

у этого тоже есть указатель на себя. Если вы создадите две небольшие строки, а затем поменяете их местами, деструкторы решат, что строка "нелокальна", и попытаются удалить память:

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

Valgrind говорит:

==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 is никогда хорошая идея, если типы тривиально копируются, то по умолчанию std::swap будет оптимальным (ваш компилятор не оптимизирует его для memcpy, а затем получит лучший компилятор), и если они не тривиально-копируемы, это отличный способ встретить г-на неопределенное поведение и его друг мистер серьезный Жук.


помимо примеров, упомянутых в других ответах (частные объекты, содержащие указатели на части себя и объекты, нуждающиеся в блокировке), может также быть случай указателей на объект, управляемый внешней структурой данных, которая должна быть обновлена соответствующим образом (обратите внимание, что пример несколько надуман, чтобы не быть чрезмерным (и, возможно, багги в силу того, что не был протестирован)):

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. Конечно, примеры реального мира для этого обычно несколько сложнее, но суть должна быть ясной.

помимо примеров, я думаю, что копирование (или замена) нетривиально копируемых объектов, подобных этому, является неопределенным поведением по стандарту (может проверить это позже). В этом случае не было бы никакой гарантии, что этот код будет работать с более сложными объектами.


некоторые еще не упомянутые:

  • своп может иметь побочные эффекты, например, вам может потребоваться обновить указатели внешних элементов, чтобы указать на новое местоположение, или сообщить слушающим объектам, что содержимое объекта изменилось.
  • замена двух элементов, которые используют относительные адреса, вызовет проблемы