Making swap faster, easier to use and exception-safe

前端 未结 5 1372
忘了有多久
忘了有多久 2020-12-15 22:49

I could not sleep last night and started thinking about std::swap. Here is the familiar C++98 version:

template 
void swap(T&a         


        
相关标签:
5条回答
  • 2020-12-15 23:09

    Some types can be swapped but cannot be copied. Unique smart pointers are probably the best example. Checking for copyability and assignability is wrong.

    If T isn't a POD type, using memcpy to copy/move is undefined behavior.


    The common idiom is to provide a method void Foo::swap(Foo& other) and a specialization of std::swap<Foo>. Note that this does not work with class templates, …

    A better idiom is a non-member swap and requiring users to call swap unqualified, so ADL applies. This also works with templates:

    struct NonTemplate {};
    void swap(NonTemplate&, NonTemplate&);
    
    template<class T>
    struct Template {
      friend void swap(Template &a, Template &b) {
        using std::swap;
    #define S(N) swap(a.N, b.N);
        S(each)
        S(data)
        S(member)
    #undef S
      }
    };
    

    The key is the using declaration for std::swap as a fallback. The friendship for Template's swap is nice for simplifying the definition; the swap for NonTemplate might also be a friend, but that's an implementation detail.

    0 讨论(0)
  • 2020-12-15 23:09

    your swap version will cause havoc if someone uses it with polymorphic types.

    consider:

    Base *b_ptr = new Base();    // Base and Derived contain definitions
    Base *d_ptr = new Derived(); // of a virtual function called vfunc()
    yourmemcpyswap( *b_ptr, *d_ptr );
    b_ptr->vfunc(); //now calls Derived::vfunc, while it should call Base::vfunc
    d_ptr->vfunc(); //now calls Base::vfunc while it should call Derived::vfunc
    //...
    

    this is wrong, because now b contains the vtable of the Derived type, so Derived::vfunc is invoked on a object which isnt of type Derived.

    The normal std::swap only swaps the data members of Base, so this is OK with std::swap

    0 讨论(0)
  • 2020-12-15 23:10

    So why not simply write a swap template that does exactly that: swap the object representations*?

    There's many ways in which an object, once being constructed, can break when you copy the bytes it resides in. In fact, one could come up with a seemingly endless number of cases where this would not do the right thing - even though in practice it might work in 98% of all cases.

    That's because the underlying problem to all this is that, other than in C, in C++ we must not treat objects as if they are mere raw bytes. That's why we have construction and destruction, after all: to turn raw storage into objects and objects back into raw storage. Once a constructor has run, the memory where the object resides is more than only raw storage. If you treat it as if it weren't, you will break some types.

    However, essentially, moving objects shouldn't perform that much worse than your idea, because, once you start to recursively inline the calls to std::move(), you usually ultimately arrive at where built-ins are moved. (And if there's more to moving for some types, you'd better not fiddle with the memory of those yourself!) Granted, moving memory en bloc is usually faster than single moves (and it's unlikely that a compiler might find out that it could optimize the individual moves to one all-encompassing std::memcpy()), but that's the price we pay for the abstraction opaque objects offer us. And it's quite small, especially when you compare it to the copying we used to do.

    You could, however, have an optimized swap() using std::memcpy() for aggregate types.

    0 讨论(0)
  • 2020-12-15 23:23

    I deem this a minor issue, because such kinds of objects probably should not have provided copy operations in the first place.

    That is, quite simply, a load of wrong. Classes that notify observers and classes that shouldn't be copied are completely unrelated. How about shared_ptr? It obviously should be copyable, but it also obviously notifies an observer- the reference count. Now it's true that in this case, the reference count is the same after the swap, but that's definitely not true for all types and it's especially not true if multi-threading is involved, it's not true in the case of a regular copy instead of a swap, etc. This is especially wrong for classes that can be moved or swapped but not copied.

    because in general, move operations are allowed to throw

    They are most assuredly not. It is virtually impossible to guarantee strong exception safety in pretty much any circumstance involving moves when the move might throw. The C++0x definition of the Standard library, from memory, explicitly states any type usable in any Standard container must not throw when moving.

    This is as efficient as it gets

    That is also wrong. You're assuming that the move of any object is purely it's member variables- but it might not be all of them. I might have an implementation-based cache and I might decide that within my class, I should not move this cache. As an implementation detail it is entirely within my rights not to move any member variables that I deem are not necessary to be moved. You, however, want to move all of them.

    Now, it's true that your sample code should be valid for a lot of classes. However, it's extremely very definitely not valid for many classes that are completely and totally legitimate, and more importantly, it's going to compile down to that operation anyway if the operation can be reduced to that. This is breaking perfectly good classes for absolutely no benefit.

    0 讨论(0)
  • 2020-12-15 23:31

    This will break class instances that have pointers to their own members. For example:

    class SomeClassWithBuffer {
      private:
        enum {
          BUFSIZE = 4096,
        };
        char buffer[BUFSIZE];
        char *currentPos; // meant to point to the current position in the buffer
      public:
        SomeClassWithBuffer();
        SomeClassWithBuffer(const SomeClassWithBuffer &that);
    };
    
    SomeClassWithBuffer::SomeClassWithBuffer():
      currentPos(buffer)
    {
    }
    
    SomeClassWithBuffer::SomeClassWithBuffer(const SomeClassWithBuffer &that)
    {
      memcpy(buffer, that.buffer, BUFSIZE);
      currentPos = buffer + (that.currentPos - that.buffer);
    }
    

    Now, if you just do memcpy(), where would currentPos point? To the old location, obviously. This will lead to very funny bugs where each instance actually uses another's buffer.

    0 讨论(0)
提交回复
热议问题