按位掉掉()是个坏主意的例子?

时间:2012-07-24 19:49:22

标签: c++ bit-manipulation swap

您不应该将对象指针视为OOP语言(包括C ++)中原始二进制数据的指针。对象“超过”它们的代表性。

因此,例如,通过交换字节来swap两个对象是不正确的:

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 个答案:

答案 0 :(得分: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  

此处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

在看到这样的事情之后,我喜欢信任我的编译器,并且避免尝试使用bit fiddling来改进代码。它没有帮助,除非分析发现了意想不到的瓶颈。

(以MSVC 10和我的std :: string实现为特色)

答案 1 :(得分:8)

我认为这几乎总是一个糟糕的想法,除非在特定情况下进行了性能分析并且swap的更明显和清晰的实现存在性能问题。即使在这种情况下,我也只会采用这种方法来实现直接的无继承结构,从来不会用于任何类。你永远不知道什么时候会增加继承可能会破坏整个事情(也可能以真正阴险的方式)。

如果你想要一个快速交换实现,或许更好的选择(在适当的情况下)是pimpl该类,然后只是交换实现(再次,这假设没有指向所有者的后向指针,但这很容易包含到课堂和课堂而不是外部因素。)

编辑:这种方法可能存在的问题:

  • 指针回归自我(直接或间接)
  • 如果类包含任何直接字节副本无意义的对象(实际上是递归此定义)或通常禁用复制的对象
  • 如果类需要任何类型的锁定来复制
  • 这里 easy 意外地传递了两种不同的类型(它只需要一个中间函数来隐式地使派生类看起来像父类)然后你交换vptrs(OUCH!)

答案 2 :(得分:3)

为什么“自我指针”会受到干扰?

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] = '\0';
  }
  ~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::streambufstd::string这样的类型,几乎没有人为的或深奥的例子。

基本上,bad_swap 从不一个好主意,如果类型可以轻易复制,那么默认的std::swap将是最佳的(编译器不优化它)然后得到一个更好的编译器,如果它们不是简单的可复制的,那么这是一个很好的方式来满足Undefined Behavior先生和他的朋友Mr. Serious Bug。

答案 3 :(得分: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交换了对象,那么显然这样会破坏。当然,现实世界的例子通常有点复杂,但重点应该是明确的。

除了示例之外,我认为复制(或交换)这样的非平凡可复制对象是标准的未定义行为(可能稍后检查)。在这种情况下,根本无法保证该代码能够处理更复杂的对象。

答案 4 :(得分:1)

有些人尚未提及:

  • 交换可能有副作用,例如,您可能必须更新外部元素的指针以指向新位置,或通知侦听对象该对象的内容已更改。
  • 交换使用相对地址的两个元素会导致问题