7

You're not supposed to treat object pointers as pointers to raw binary data in OOP languages, including C++. Objects are "more than" their representation.

So, for example, swaping two objects by swapping their bytes is incorrect:

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));
}

The only situation, however, in which I can imagine this shortcut causing a problem is when an object contains a pointer to itself, which I have rarely (never?) seen in practice; there may, though, also be other scenarios.

What are some actual (real-world) examples of when a correct swap would break if you performed a bitwise swap?
I can easily come up with contrived examples with self-pointers, but I can't think of any real-world ones.

user541686
  • 205,094
  • 128
  • 528
  • 886
  • 3
    This is perfectly legal for trivially-copyable types. C++ is not a strictly "OOP language", and trivially-copyable types are _not_ "more than their representation". – ildjarn Jul 24 '12 at 19:57
  • 7
    When is it ever a *good* idea? I'm sure that whenever a binary copy or swap is ok, the compiler will generate that. – Bo Persson Jul 24 '12 at 20:04
  • @BoPersson: I'm glad you're sure, because I'm not... – user541686 Jul 24 '12 at 20:07
  • 4
    a commonly used object which uses a pointer to itself is std::string, for optimization, several implementations contain a short string buffer which is part of the main 'struct'. if the length grows beyond the size of this buffer, a dynamically allocated buffer is used. there is one pointer, which either points to the internal short string, or to the dynamically allocated buffer. – Willem Hengeveld Jul 24 '12 at 20:13
  • 3
    I don't know what compilers you are using, but the ones I have seen are smart enough to inline the `memcpy`, and to generate the same code for a struct assignment (or for a loop copying an array). – Bo Persson Jul 24 '12 at 20:14
  • @BoPersson: To be honest, I've never checked. But I'm not quite so sure the compiler can always figure it out either. – user541686 Jul 24 '12 at 20:15
  • another reason why you should not just swap ( or copy ) objects, is that you may end up with an object which does not follow the cpu's alignment rules. – Willem Hengeveld Jul 24 '12 at 20:15
  • another remark, it is not the swapping, but the copying which can have undesireable effects. – Willem Hengeveld Jul 24 '12 at 20:18
  • @WillemHengeveld: I don't get the alignment-rule comment (why would it cause alignment problems?), but for the string comment, you're definitely right... feel free to post that as an answer! – user541686 Jul 24 '12 at 20:18
  • A similar question [Making swap faster, easier to use, and exception safe?](http://stackoverflow.com/questions/4875289/making-swap-faster-easier-to-use-and-exception-safe?rq=1) – Bo Persson Jul 24 '12 at 20:23
  • @BoPersson: Yeah, that's what prompted me to ask for examples. :) I also put a comment, seems like sbi replied a few minutes ago... I just let him know about this question. – user541686 Jul 24 '12 at 20:24
  • By default, some of MSVC's debug iterators contain pointers to the object they iterate over. – Mooing Duck Jul 24 '12 at 21:20
  • 1
    Note that the problem of swapping a self-pointer affects not just the object with the self-pointer, but any other object that *contains* such an object. – Mark Ransom Jul 24 '12 at 21:41

5 Answers5

13

This is not specifically about swap but an example showing that low level optimizations are maybe not worth the trouble. The compiler often figures it out anyway.

Of course, this is my favorite example where the compiler is exceptionally lucky, but anyway we shouldn't assume that compilers are stupid and that we can easily improve on the generated code with some simple tricks.

My test code is - construct a std::string and copy it.

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

The first constructor looks like this

  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);
     }
  }

The generated code is

   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  

Here traits_type::copycontains a call to memcpy, which is optimized into a single register copy of the whole string (carefully selected to fit). The compiler also transforms a call to strlen into a compile time 8.

Then we copy it into a new string. The copy constructor looks like this

  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());
     }
  }

and results in just 4 machine instructions:

   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  

Note that the optimizer remembers that the char's are still in register rdx and that the string length must be the same, 8.

It is after seeing things like this that I like to trust my compiler, and avoid trying to improve code with bit fiddling. It doesn't help, unless profiling finds an unexpected bottleneck.

(featuring MSVC 10 and my std::string implementation)

Bo Persson
  • 90,663
  • 31
  • 146
  • 203
  • I've seen you mention your stdlib implementation a few times now -- is it available somewhere for perusal? – ildjarn Jul 24 '12 at 22:22
  • No, it is just a partial implementation I play with on my spare time. Mentioned it here because the code doesn't match what comes with the compiler. – Bo Persson Jul 24 '12 at 22:32
  • 2
    Offering some more information - I don't contribute code to open source projects that require copyright reassignment, because I have already formally reassigned all my copyright to my current employer (a bank, with lots if lawyers). Just to be on the safe side, I also avoid publishing any larger volumes of code, in order not to get into trouble over some legal technicality. – Bo Persson Jul 24 '12 at 23:24
8

I'm going to argue that this is almost always a bad idea except in the specific case where profiling has been done and a more obvious and clear implementation of swap has performance problems. Even in that case I would only go with this sort of approach for straight up no-inheritance structures, never for any sort of class. You never know when inheritance will be added potentially breaking the whole thing (possibly in truly insidious ways too).

If you want a fast swap implementation perhaps a better choice (where appropriate) is to pimpl the class and then just swap out the implementation (again, this assumes that there are no back-pointers to the owner, but that's easily contained to the class & impl rather than external factors).

EDIT: Possible problems with this approach:

  • Pointers back to self (directly or indirectly)
  • If the class contains any object for which a straight byte-copy is meaningless (effectively recursing this definition) or for which copying is normally disabled
  • If the class needs any sort of locking to copy
  • It's easy to accidentally pass in two different types here (all it takes is one intermediate function to implicitly make a derived class look like the parent) and then you swap vptrs (OUCH!)
Mark B
  • 95,107
  • 10
  • 109
  • 188
  • Those 4 bullets could be reduced down to 2: (1) Self-pointers (which I already mentioned seem to be rare, until William pointed out one class in which it's common), and (2) Locks (could you expand on this? I was not assuming threading, since until C++11, C++ doesn't even have a threading model.). #3 is just basically #2, and #4 is just completely missing my comment in the code. – user541686 Jul 24 '12 at 20:31
  • @Mehrdad: #3 is not the same as #2. #2 is about types which might have some inherent magic making them not copyable, while #3 is about classes designed to be used from multiple threads, where changing the object without locking can be quite problematic to say the least. – Grizzly Jul 24 '12 at 20:34
  • @Grizzly: Huh? How is a mutex not some "sort of lock"? (Also I'm not talking about copying, I'm talking about swapping.) – user541686 Jul 24 '12 at 20:34
  • @Mehrdad: thats not the point. One bullets is about objects not easily copyable (and memcopy is very much a copy, even used for swapping purposes), while the other is about additional operations being necessary during the copy. Having a mutex contained inside the class doesn't necessarily mean it needs to be locked for copying, neither does the need to lock imply that the class contains a mutex (e.g. the lock might be shared between several objects) – Grizzly Jul 24 '12 at 20:38
  • I'm not sure I understand what you're trying to say. You're basically saying "if the object is magical and doesn't let you `memcpy` it then you can't `memcpy` it", which seems a bit of a tautology to me. Could you actually give a concrete example? – user541686 Jul 24 '12 at 20:41
  • @Mehrdad It is so easy to violate your assumption regarding both being the most derived types I felt obligated to mention it anyway. – Mark B Jul 24 '12 at 20:48
3

Why are "self-pointers" contrived?

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

This type holds a buffer and a current position into the buffer.

Or maybe you've heard of iostreams:

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

As someone else mentioned, the small string optimisation:

// 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; }
  // ...
};

This has a self-pointer too. If you construct two small strings then swap them, the destructors will both decide the string is "non-local" and try to delete the memory:

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

Valgrind says:

==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

So that shows it affects types like std::streambuf and std::string, hardly contrived or esoteric examples.

Basically, bad_swap is never a good idea, if the types are trivially-copyable then the default std::swap will be optimal (of your compiler doesn't optimise it to memcpy then get a better compiler) and if they're not trivially-copyable it's a great way to meet Mr. Undefined Behaviour and his friend Mr. Serious Bug.

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
2

Besides the examples mentioned in other answers (particulary objects containing pointers to parts of themselves and objects needing locking), there could also be a case of pointers to the object being managed by an external datastructure, which needs to be updated accordingly (please note that the example is somewhat contrived in order to not be excessive (and maybe buggy by virtue of not having been tested)):

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); }
   ...
};

obviously something like this would break badly if objects are swapped by memcpy. Of course real world examples for this are typically somewhat more complex, but the point should be clear.

Besides the examples I think that copying (or swapping) non trivially copyable objects like this is undefined behaviour by the standard (might check that later). In that case there would be no guarantee at all for that code to work with more complex objects.

Grizzly
  • 19,595
  • 4
  • 60
  • 78
  • 1
    The issue of updating pointers outside of the object exists whether the swap uses `memcpy` or not. – Mark Ransom Jul 24 '12 at 21:37
  • @MarkRansom: I was referring to the implementation given in the question. Of course the problem exists for any scenario which doesn't use proper copy/move constructors/assignment operators – Grizzly Jul 24 '12 at 21:47
1

Some not already mentioned:

  • The swap may have side effects, for example you may have to update the pointers of external elements to point at the new location, or inform listening objects that the contents of the object have changed.
  • Swapping two elements that use relative addresses would cause problems
josefx
  • 15,506
  • 6
  • 38
  • 63
  • +1 for relative addressing (though it's worth mentioning it's basically just an external pointer in disguise) – user541686 Jul 24 '12 at 20:42