2

I propose this implementation of swap, if valid, is superior to the current implementation of std::swap:

#include <new>
#include <type_traits>

template<typename T>
auto swap(T &t1, T &t2) ->
    typename std::enable_if<
        std::is_nothrow_move_constructible<T>::value
    >::type
{
    alignas(T) char space[sizeof(T)];
    auto asT = new(space) T{std::move(t1)};
        // a bunch of chars are allowed to alias T
    new(&t1) T{std::move(t2)};
    new(&t2) T{std::move(*asT)};
}

The page for std::swap at cppreference, implies it uses move-assignment, because the noexcept specification depends on whether the move-assignment is no-throw. Furthermore, it has been asked here how is swap implemented and that is what I see in the implementations for libstdc++ and libc++

template<typename T>
void typicalImplementation(T &t1, T &t2)
    noexcept(
        std::is_nothrow_move_constructible<T>::value &&
        std::is_nothrow_move_assignable<T>::value
    )
{
    T tmp{std::move(t1)};
    t1 = std::move(t2);
        // this move-assignment may do work on t2 which is
        // unnecessary since t2 will be reused immediately
    t2 = std::move(tmp);
        // this move-assignment may do work on tmp which is
        // unnecessary since tmp will be immediately destroyed
    // implicitly tmp is destroyed
}

I dislike using move-assignment as in t1 = std::move(t2) because that implies executing the code to release the resources held in t1 if resources are held, even though it is known the resources in t1 where released already. I have a practical case in which releasing resources occur across a virtual method call, thus the compiler can't eliminate that unnecessary work because it can't know the virtual override code, whatever it is, won't do anything because there are no resources to release in t1.

If this is illegal in practice, could you please point out what it violates in the standard?

So far, I have seen in the answers and comments two objections that may make this illegal:

  1. The ephemeral object created in tmp is not destructed, but there may be some assumption in user code that if a T is constructed it will be destructed
  2. T may be a type with constants or references that can't be changed, the move assignment may be implemented swapping the resources without touching those constants or rebinding references.

Thus, it seems this construct is legal for any type except those that either hit case 1 or 2 above.

For illustration, I put a link to a compiler explorer page which shows all three implementations for the case of swapping vectors of ints, that is, the typical default implementation of std::swap, the specialized for vector, and the one I am proposing. You may see the proposed implementation performs less work than the typical, exactly the same as the specialized in the standard.

Only the user can decide to swap doing "all-move-construction" versus "one move construction, two move assignments", your answers inform users of then the "all-move-construction" is not valid.

After more side-band conversations with colleagues, what I am asking boils down to this works for types in which move can be seen as destructive, hence there is no need to balance the constructions with destructions.

TheCppZoo
  • 1,131
  • 5
  • 12
  • Move assignment can be faster than move construction. And I'm pretty sure if you placement new into memory of a valid object, then that valid object is never destructed. – xaxxon Nov 05 '18 at 21:23
  • Per the part of the standard that Rakete mentions below, using this form on any type which depends on its destructor is UB. – xaxxon Nov 05 '18 at 21:33
  • 1
    std::swap also does not make the user think that constructors would be called, so the behavior is quite misleading. Say you have an object instance counter in your type.. every time you call swap it goes up by 3? That doesn't make any sense for a "swap" – xaxxon Nov 05 '18 at 21:40
  • @xaxxon this is the link for how libstdc++ implements swap which uses move constructors: https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/move.h#L193 – TheCppZoo Nov 05 '18 at 22:27
  • It only uses the move constructor when it must - when it's constructing a new object (__tmp) - after that, it uses move assignment. – xaxxon Nov 05 '18 at 22:32
  • 1
    The line `auto lt1 = std::launder(&t1);` makes `lt1` usable to access the new object, but it has no effect on `t1`, so it's still UB to use `t1` to access the new object. – alain Nov 05 '18 at 22:32
  • The move constructor may also do "unnecessary work" to the object being moved out of just as the assignment might. – xaxxon Nov 05 '18 at 22:35

2 Answers2

2

It is illegal if T has ref or const members. Use std::launder or store the newed pointer (see [basic.life]p8)

auto asT = new(space) T{std::move(t1)};
// or use std::launder

But you also need to use std::launder for t1 and t2! And here you have a problem, because without std::launder, t1 and t2 refer to the old (already destructed) value and don't refer to the newly constructed object. Any access to t1 and t2 is then UB.

I dislike using move-assignment in swap because it implies the destruction of already-moved objects, which seems unnecessary work, hence this implementation.

Premature optimization? Now you need to call two destructors (t1 and t2)! Also, a destructor call is really not expensive.

Right now, as Nathan Oliver said, the destructors are not being called, (which is not UB), but you really shouldn't be doing that as the destructor might do important stuff.

NathanOliver
  • 150,499
  • 26
  • 240
  • 331
Rakete1111
  • 42,521
  • 11
  • 108
  • 141
  • 1
    They also need to call `t1`'s destructor before they call `new(&t1) T{std::move(t2)};` don't they? (same Q applies to t2 as well) – NathanOliver Nov 05 '18 at 21:28
  • @NathanOliver They don't actually, but you're right, it's a bad idea if you don't ;) – Rakete1111 Nov 05 '18 at 21:28
  • it's legal for the lifetime of an object to end without its destructor being called? – xaxxon Nov 05 '18 at 21:30
  • @xaxxon jup, even if it is non-trivial http://eel.is/c++draft/basic.life#5.sentence-2 – Rakete1111 Nov 05 '18 at 21:31
  • 1
    ok... so the important part is "...any program that depends on the side effects produced by the destructor has undefined behavior." It seems like you should call that out clearly in your answer. – xaxxon Nov 05 '18 at 21:32
  • @Rakete1111 My reading of that paragraph leads me to beleive that outside of swap, when the object is destroyed and the destructor is called and then it is UB. Is that not the case? – NathanOliver Nov 05 '18 at 21:34
  • 1
    @xaxxon My understanding is that that part is useless. You can't depend on something that the standard doesn't say. It's like having a sentence that says "Any program that relies on "1 + 1" calling a function f() has UB." – Rakete1111 Nov 05 '18 at 21:35
  • @NathanOliver That is also the case. The problem is that the t variable don't refer to the new object. They still refer to the old value. So, when the implicitly called destructor is called, you access the object and you get UB – Rakete1111 Nov 05 '18 at 21:35
  • 3
    @Rakete1111 In a well-defined program, side effects from the destructor of an object must be occur when the object's lifetime ends, right? So if you're going to skip calling the destructor, your must know, a priori, that there are no side effects - which is a poor choice in generic code like std::swap – xaxxon Nov 05 '18 at 21:38
  • @xaxxon noo that's what that sentence says. You don't need to call it. Yes, that is the point. You need to call it in generic code, because it could have side effects. – Rakete1111 Nov 05 '18 at 21:40
  • @Rakete1111 I put in the summary the three objections you raised, 1. launder, 2. const, reference instance members, 3. destructor side effects. It makes it still legal for very many types if the users don't trigger the UB. – TheCppZoo Nov 05 '18 at 23:06
  • @TheCppZoo Actually, only const and reference members are a problem (and if they are nested but that would be weird). But for a generic function, that's a no go. – Rakete1111 Nov 05 '18 at 23:10
  • @Rakete1111 the traits to exclude those two cases don't exist, but the code I propose is a valid choice for users to swap this way if they know their types don't hit 1 or 2. Also, there is no way to find out if all move-construction swap is more efficient than one move construction and two move-assignments, it is still the user's choice – TheCppZoo Nov 05 '18 at 23:18
  • @Rakete1111 it seems you said in a comment by virtue of "receiving" a construction, `t1` and `t2` are invalid out of `swap`, however, http://eel.is/c++draft/basic.life#8.sentence-1 says they are valid if you accept the lifetime of the original objects ended when being the recipients of placement-new and 6.8.3 is not applicable... – TheCppZoo Nov 05 '18 at 23:43
  • @TheCppZoo They are only invalid if they have const or ref members (when they don't satisfy p8), otherwise they are perfectly fine :) – Rakete1111 Nov 05 '18 at 23:47
  • @Rakete1111 "_only invalid if_" Why are you even suggesting `std::launder`? – curiousguy Nov 06 '18 at 22:07
  • @curiousguy because to access the new values through the ts you need launder – Rakete1111 Nov 06 '18 at 22:09
  • @Rakete1111 No programmer can be expected to realize he should use `std::launder` after calling a function called `swap`, do you agree? – curiousguy Nov 06 '18 at 22:52
  • @curiousguy I completely agree. :) – Rakete1111 Nov 06 '18 at 23:01
  • @Rakete1111 So in practice, no type for which placement new isn't guaranteed to preserve the validity of existing pointers and reference should use this swap technique. That means you would not "*need to use std::launder for t1 and t2!*" – curiousguy Nov 06 '18 at 23:10
  • @curiousguy well yes, but for a generic swap it's not really a good idea – Rakete1111 Nov 06 '18 at 23:20
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/183223/discussion-between-curiousguy-and-rakete1111). – curiousguy Nov 06 '18 at 23:28
2

Note that move constructors and assignment operators are required to leave their argument in a valid state. Typically, the implementation will default construct state then swap with the argument's state to steal its resources. Depending on the invariants the object wants to maintain, this may still leave the argument owning resources that it is relying on the destructor to reclaim. If destruction is elided, these will leak.

e.g. consider:

class X
{
public:
    X(): resource_( std::move( allocate_resource() ) )

    X( X&& other ): X()
    {
        std::swap( resource_, other.resource_ );
    }

private:
    std::shared_ptr<Y> resource_;
};

then,

X a;
X b;
swap( a, b );

Now, if swap is implemented as you propose, at the point where you do

new(&t2) T{std::move(*asT)};

we leak an instance of the resource, since the move constructor allocates one at *asT to replace the one it stole, and this is never destructed.

Another way of looking at this is that either the destruction does nothing, and so is cheap/free and so does not justify mystery-meat optimizations, or the destruction is expensive enough to care about, in which case it's doing something and so going behind the object's back to avoid doing that thing is morally wrong and will lead to badness down the line; fix the object implementation instead.

moonshadow
  • 75,857
  • 7
  • 78
  • 116
  • "move constructors and assignment operators are required to leave their argument in a valid state." What do you mean by "valid"? They must be destructible for sure, but are there actually other requirements? – xaxxon Nov 05 '18 at 21:50
  • xaxxon https://stackoverflow.com/questions/12095048/what-constitutes-a-valid-state-for-a-moved-from-object-in-c11 – moonshadow Nov 05 '18 at 21:54
  • @moonshadow As you can see, all operations in the proposed `swap` leave the objects in a valid state (only move constructions of valid objects happen). A resource leak is not possible if: 1. the move constructors don't leak and 2. there is no resource that is acquired at move-construction that is not transferred as the source of move-construction (which would require releasing at the destructor), I think the scenario ("magical resources" acquired at construction but not released being the source of move-construction) won't limit the users of my code too much – TheCppZoo Nov 05 '18 at 23:14