programing

왜 어떤 사람들은 이동 할당에 스왑을 사용합니까?

itsource 2021. 1. 14. 08:18
반응형

왜 어떤 사람들은 이동 할당에 스왑을 사용합니까?


예를 들어 stdlibc ++에는 다음이 있습니다.

unique_lock& operator=(unique_lock&& __u)
{
    if(_M_owns)
        unlock();
    unique_lock(std::move(__u)).swap(*this);
    __u._M_device = 0;
    __u._M_owns = false;
    return *this;
}

두 개의 __u 멤버를 * this에 직접 할당하지 않는 이유는 무엇입니까? 스왑은 __u가 * this 멤버에 할당된다는 것을 의미하지 않으며, 나중에 0과 false를 할당 한 경우에만 스왑이 불필요한 작업을 수행합니다. 내가 무엇을 놓치고 있습니까? (unique_lock :: swap은 각 멤버에서 std :: swap을 수행합니다)


그것은 내 잘못이야. (반 농담, 반은 아닙니다).

이동 할당 연산자의 예제 구현을 처음 보여줄 때 방금 스왑을 사용했습니다. 그런 다음 어떤 똑똑한 사람 (누가 기억 나지 않습니다)은 할당 전에 lhs를 파괴하는 부작용이 중요 할 수 있다고 지적했습니다 (예 : unlock ()). 그래서 이동 할당을 위해 스왑 사용을 중단했습니다. 그러나 스왑 사용의 역사는 여전히 남아 있습니다.

이 예에서는 스왑을 사용할 이유가 없습니다. 제안한 것보다 덜 효율적입니다. 실제로 libc ++ 에서 나는 당신이 제안한 것을 정확하게 수행합니다.

unique_lock& operator=(unique_lock&& __u)
    {
        if (__owns_)
            __m_->unlock();
        __m_ = __u.__m_;
        __owns_ = __u.__owns_;
        __u.__m_ = nullptr;
        __u.__owns_ = false;
        return *this;
    }

일반적으로 이동 할당 연산자는 다음을 수행해야합니다.

  1. 가시적 인 리소스를 파괴합니다 (구현 세부 리소스를 저장할 수 있음).
  2. 이동은 모든 기지와 구성원을 할당합니다.
  3. 기지와 구성원의 이동 할당이 rhs를 리소스없이 만들지 않았다면 그렇게 만드십시오.

이렇게 :

unique_lock& operator=(unique_lock&& __u)
    {
        // 1. Destroy visible resources
        if (__owns_)
            __m_->unlock();
        // 2. Move assign all bases and members.
        __m_ = __u.__m_;
        __owns_ = __u.__owns_;
        // 3. If the move assignment of bases and members didn't,
        //           make the rhs resource-less, then make it so.
        __u.__m_ = nullptr;
        __u.__owns_ = false;
        return *this;
    }

최신 정보

댓글에는 이동 생성자를 처리하는 방법에 대한 후속 질문이 있습니다. 나는 거기에서 (주석으로) 대답하기 시작했지만 형식과 길이 제한으로 인해 명확한 응답을 만들기가 어렵습니다. 따라서 여기에 내 응답을 넣습니다.

문제는 이동 생성자를 만드는 데 가장 적합한 패턴은 무엇입니까? 기본 생성자에게 위임 한 다음 교체 하시겠습니까? 이것은 코드 중복을 줄이는 이점이 있습니다.

내 대답은 다음과 같습니다. 가장 중요한 점은 프로그래머가 생각없이 패턴을 따르는 것을 조심해야한다는 것입니다. 이동 생성자를 default + swap으로 구현하는 것이 정답 인 클래스가있을 수 있습니다. 수업은 크고 복잡 할 수 있습니다. A(A&&) = default;잘못된 일을 할 수 있습니다. 각 수업에 대한 모든 선택을 고려하는 것이 중요하다고 생각합니다.

OP의 예를 자세히 살펴 보겠습니다 std::unique_lock(unique_lock&&)..

관찰 :

A.이 수업은 매우 간단합니다. 두 개의 데이터 멤버가 있습니다.

mutex_type* __m_;
bool __owns_;

B.이 클래스는 알 수없는 수의 클라이언트가 사용하는 범용 라이브러리에 있습니다. 이러한 상황에서는 성능 문제가 최우선 순위입니다. 클라이언트가 성능에 중요한 코드에서이 클래스를 사용할지 여부를 알 수 없습니다. 그래서 우리는 그들이 있다고 가정해야합니다.

C.이 클래스의 이동 생성자는 무엇이든 상관없이 적은 수의로드 및 저장으로 구성됩니다. 따라서 성능을 확인하는 좋은 방법은로드 및 스토어를 계산하는 것입니다. 예를 들어, 4 개의 상점으로 작업을 수행하고 다른 누군가가 2 개의 상점으로 만 동일한 작업을 수행하는 경우 두 구현 모두 매우 빠릅니다. 그러나 그들의 것은 당신 의 것 보다 두 배 빠릅니다! 그 차이는 일부 클라이언트의 긴밀한 루프에서 중요 할 수 있습니다.

먼저 기본 생성자와 멤버 스왑 함수에서 count로드 및 저장을 허용합니다.

// 2 stores
unique_lock()
    : __m_(nullptr),
      __owns_(false)
{
}

// 4 stores, 4 loads
void swap(unique_lock& __u)
{
    std::swap(__m_, __u.__m_);
    std::swap(__owns_, __u.__owns_);
}

이제 이동 생성자를 두 가지 방법으로 구현해 보겠습니다.

// 4 stores, 2 loads
unique_lock(unique_lock&& __u)
    : __m_(__u.__m_),
      __owns_(__u.__owns_)
{
    __u.__m_ = nullptr;
    __u.__owns_ = false;
}

// 6 stores, 4 loads
unique_lock(unique_lock&& __u)
    : unique_lock()
{
    swap(__u);
}

첫 번째 방법은 두 번째 방법보다 훨씬 복잡해 보입니다. 그리고 소스 코드는 더 크고 우리가 이미 다른 곳에서 작성한 코드를 다소 복제합니다 (예 : 이동 할당 연산자). 즉, 버그가 발생할 가능성이 더 많습니다.

두 번째 방법은 더 간단하고 이미 작성한 코드를 재사용하는 것입니다. 따라서 버그 가능성이 적습니다.

첫 번째 방법이 더 빠릅니다. 적재 및 저장 비용이 거의 같으면 66 % 더 빠릅니다!

이것은 고전적인 엔지니어링 트레이드 오프입니다. 공짜 점심은 없습니다. 그리고 엔지니어들은 트레이드 오프에 대한 결정을 내려야하는 부담에서 결코 벗어날 수 없습니다. 그 순간 비행기가 공중에서 떨어지기 시작하고 원자력 발전소가 녹기 시작합니다.

들어 libc의 ++ , 나는 빠른 솔루션을 선택했다. 제 이론적 근거는이 수업의 경우에 상관없이 올바르게하는 것이 좋습니다. 수업은 충분히 간단해서 내가 제대로 할 가능성이 높습니다. 제 고객들은 성과를 중시 할 것입니다. 다른 맥락에서 다른 클래스에 대해 또 다른 결론을 내릴 수 있습니다.


예외 안전에 관한 것입니다. __u연산자가 호출 될 때 이미 생성 되었기 때문에 예외 swap가없고 던지지 않습니다.

구성원 할당을 수동으로 수행 한 경우 각각 예외가 발생할 수있는 위험을 감수하고 부분적으로 이동 할당을 받았지만 구제 조치를 취해야합니다.

이 사소한 예에서는 표시되지 않지만 일반적인 디자인 원칙입니다.

  • copy-construct 및 swap에 의한 복사 할당.
  • move-construct 및 swap에 의한 이동 할당.
  • +구문 및 +=등으로 작성하십시오 .

기본적으로 "실제"코드의 양을 최소화하고 핵심 기능 측면에서 가능한 한 많은 다른 기능을 표현하려고합니다.

(이는 unique_ptr복사 생성 / 할당을 허용하지 않기 때문에 할당에서 명시적인 rvalue 참조를 사용하므로이 디자인 원칙의 가장 좋은 예가 아닙니다.)


트레이드 오프와 관련하여 고려해야 할 또 다른 사항 :

default-construct + swap 구현은 더 느리게 보일 수 있지만 컴파일러의 데이터 흐름 분석은 무의미한 할당을 제거하고 손으로 쓴 코드와 매우 유사하게 끝날 수 있습니다. 이것은 "영리한"값 의미 체계가없는 유형에 대해서만 작동합니다. 예로서,

 struct Dummy
 {
     Dummy(): x(0), y(0) {} // suppose we require default 0 on these
     Dummy(Dummy&& other): x(0), y(0)
     {
         swap(other);             
     }

     void swap(Dummy& other)
     {
         std::swap(x, other.x);
         std::swap(y, other.y);
         text.swap(other.text);
     }

     int x, y;
     std::string text;
 }

generated code in move ctor without optimization:

 <inline std::string() default ctor>
 x = 0;
 y = 0;
 temp = x;
 x = other.x;
 other.x = temp;
 temp = y;
 y = other.y;
 other.y = temp;
 <inline impl of text.swap(other.text)>

This looks awful, but data flow analysis can determine it is equivalent to the code:

 x = other.x;
 other.x = 0;
 y = other.y;
 other.y = 0;
 <overwrite this->text with other.text, set other.text to default>

Maybe in practice compilers won't always produce the optimal version. Might want to experiment with it and take a glance at the assembly.

There are also cases when swapping is better than assigning because of "clever" value semantics, for example if one of the members in the class is a std::shared_ptr. No reason a move constructor should mess with the atomic refcounter.


I will answer the question from header: "Why do some people use swap for move assignments?".

The primary reason to use swap is providing noexcept move assignment.

From Howard Hinnant's comment:

In general a move assignment operator should:
1. Destroy visible resources (though maybe save implementation detail resources).

But in general destroy/release function can fail and throw exception!

Here is an example:

class unix_fd
{
    int fd;
public:
    explicit unix_fd(int f = -1) : fd(f) {}
    ~unix_fd()
    {
        if(fd == -1) return;
        if(::close(fd)) /* !!! call is failed! But we can't throw from destructor so just silently ignore....*/;
    }

    void close() // Our release-function
    {
        if(::close(fd)) throw system_error_with_errno_code;
    }
};

Now let's compare two implementaions of move-assignment:

// #1
void unix_fd::operator=(unix_fd &&o) // Can't be noexcept
{
    if(&o != this)
    {
        close(); // !!! Can throw here
        fd = o.fd;
        o.fd = -1;
    }
    return *this;
}

and

// #2
void unix_fd::operator=(unix_fd &&o) noexcept
{
    std::swap(fd, o.fd);
    return *this;
}

#2 is perfectly noexcept!

Yes, close() call can be "delayed" in case #2. But! If we want strict error checking we must use explicit close() call, not destructor. Destructor releases resource only in "emergency" situations, where exeption can't be thrown anyway.

P.S. See also discussion here in comments

ReferenceURL : https://stackoverflow.com/questions/6687388/why-do-some-people-use-swap-for-move-assignments

반응형