7

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

因此,例如,swap通过交换它们的字节来 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  

这里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's 仍在寄存器rdx中,并且字符串长度必须相同,8

正是在看到这样的事情之后,我才喜欢相信我的编译器,并避免尝试用一些小技巧来改进代码。它没有帮助,除非分析发现意外的瓶颈。

(具有 MSVC 10 和我的 std::string 实现)

于 2012-07-24T21:05:56.413 回答
8

我要争辩说,这几乎总是一个主意,除非在已经完成分析并且更明显和更清晰的实现swap有性能问题的特定情况下。即使在那种情况下,我也只会对直接的无继承结构采用这种方法,而不是对任何类型的类。您永远不知道何时添加继承可能会破坏整个事物(也可能以真正阴险的方式)。

如果您想要快速交换实现,也许更好的选择(在适当的情况下)是 pimpl 类,然后只换出实现(同样,这假设没有指向所有者的反向指针,但这很容易包含在类中& impl 而不是外部因素)。

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

  • 指向自我的指针(直接或间接)
  • 如果该类包含任何直接字节复制无意义的对象(有效地递归此定义)或通常禁用复制的对象
  • 如果班级需要任何形式的锁定来复制
  • 在这里很容易意外传入两种不同的类型(只需要一个中间函数来隐式地使派生类看起来像父类),然后交换 vptrs(哎呀!)
于 2012-07-24T20:18:00.927 回答
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将是最佳的(您的编译器不会将其优化为 memcpy 然后获得更好的编译器),如果它们不是可简单复制的,那就太好了认识未定义行为先生和他的朋友严重错误先生的方式。

于 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

一些尚未提及的:

  • 交换可能会产生副作用,例如您可能必须更新外部元素的指针以指向新位置,或者通知侦听对象该对象的内容已更改。
  • 交换两个使用相对地址的元素会导致问题
于 2012-07-24T20:40:24.887 回答