C ++에서 이동 생성자의 동기 부여 및 사용


17

나는 최근 C ++에서 이동 생성자에 대해 읽었으며 (예 : here 참조 ) 작동 방식과 사용시기를 이해하려고합니다.

내가 이해하는 한, 이동 생성자는 큰 객체를 복사하여 발생하는 성능 문제를 완화하는 데 사용됩니다. 위키 백과 페이지에 따르면, "C ++ 03의 만성적 인 성능 문제는 값이 불필요하게 객체를 전달할 때 암시 적으로 발생할 수있는 비용이 많이 들고 불필요한 딥 카피입니다."

나는 보통 그런 상황을 해결한다

  • 객체를 참조로 전달하거나
  • 스마트 포인터 (예 : boost :: shared_ptr)를 사용하여 객체를 전달합니다 (스마트 포인터는 객체 대신 복사됩니다).

위의 두 기술이 충분하지 않고 이동 생성자를 사용하는 것이 더 편리한 상황은 무엇입니까?


1
이동 시맨틱이 훨씬 더 많은 것을 달성 할 수 있다는 사실 외에도 (답변에 언급 된 바와 같이) 참조 또는 스마트 포인터로 전달하는 것만으로는 충분하지 않은 상황이 무엇인지 묻지 말아야합니다. 그렇게하려면 (신은 shared_ptr빠른 복사를 위해서만 주의해야합니다 ) 이동 시맨틱은 코딩, 시맨틱 및 청결-벌금이 거의없이 동일하게 달성 될 수 있습니다.
— Chris는 Reinstate Monica

답변:


16

Move 의미론은 C ++에 전체 차원을 도입합니다. 값을 저렴하게 반환 할 수있는 것은 아닙니다.

예를 들어, 이동 시맨틱 std::unique_ptr이 없으면 작동하지 않습니다. std::auto_ptr이동 시맨틱을 도입하여 더 이상 사용되지 않고 C ++ 17에서 제거되었습니다. 리소스 이동은 복사와 크게 다릅니다. 고유 한 항목의 소유권을 양도 할 수 있습니다 .

예를 들어, std::unique_ptr상당히 잘 논의되었으므로을 보지 마십시오 . OpenGL에서 Vertex Buffer Object를 보자. 정점 버퍼는 GPU의 메모리를 나타냅니다. 특별한 기능을 사용하여 할당 및 할당 해제해야 할 수 있습니다. 또한 한 명의 소유자 만 사용하는 것이 중요합니다.

class vertex_buffer_object
{
    vertex_buffer_object(size_t size)
    {
        this->vbo_handle = create_buffer(..., size);
    }

    ~vertex_buffer_object()
    {
        release_buffer(vbo_handle);
    }
};

void create_and_use()
{
    vertex_buffer_object vbo = vertex_buffer_object(SIZE);

    do_init(vbo); //send reference, do not transfer ownership

    renderer.add(std::move(vbo)); //transfer ownership to renderer
}

이제이 작업을 수행 할 수 std::shared_ptr는 있지만이 리소스는 공유 할 수 없습니다. 이것은 공유 포인터를 사용하는 것을 혼란스럽게 만듭니다. 를 사용할 수는 std::unique_ptr있지만 여전히 이동 의미론이 필요합니다.

분명히, 이동 생성자를 구현하지 않았지만 아이디어를 얻었습니다.

여기서 중요한 것은 일부 리소스는 복사 할 수 없다는 것 입니다. 이동하는 대신 포인터를 전달할 수 있지만 unique_ptr을 사용하지 않으면 소유권 문제가 있습니다. 코드의 의도가 가능한 한 명확하게하는 것이 가치가 있으므로 이동 생성자가 아마도 가장 좋은 방법 일 것입니다.


답변 해주셔서 감사합니다. 여기서 공유 포인터를 사용하면 어떻게됩니까?
— 조르지오

공유 포인터를 사용하면 객체의 수명을 제어 할 수 없지만 객체는 특정 시간 동안 만 살 수 있어야합니다.
— 조르지오

3
@Giorgio 공유 포인터를 사용할 수 있지만 의미 상 잘못되었습니다. 버퍼를 공유 할 수 없습니다. 또한 vbo가 기본적으로 GPU 메모리에 대한 고유 포인터이므로 포인터를 포인터로 전달해야합니다. 나중에 코드를 보는 사람이 왜 '여기에 공유 포인터가 있습니까? 공유 리소스입니까? 버그일지도 모른다! '. 원래 의도가 무엇인지 가능한 한 명확하게하는 것이 좋습니다.
— Max

@Giorgio 네, 그것은 또한 요구 사항의 일부입니다. 이 경우 '렌더러'가 일부 리소스 (GPU의 새 객체에 대한 메모리가 부족할 수 있음)를 할당 해제하려는 경우 메모리에 대한 다른 핸들이 없어야합니다. 범위를 벗어나는 shared_ptr을 사용하면 다른 곳에 두지 않아도 작동하지만 가능할 때 완전히 명확하게 나타내지 않는 이유는 무엇입니까?
— Max

@Giorgio 또 다른 설명을 위해 편집 내용을 참조하십시오.
— Max

5

이동 의미론은 값을 반환 할 때 반드시 크게 개선되는 것은 아닙니다. 그리고 shared_ptr(또는 비슷한 것을 사용하는 경우 ) 아마도 비관적 일 것입니다. 실제로 거의 모든 합리적으로 현대적인 컴파일러는 RVO (Return Value Optimization) 및 NRVO (Named Return Value Optimization)라는 기능을 수행합니다. 즉, 실제로 값 을 복사하는 대신 값을 반환 할 때, 그들은 단순히 반환 후 값이 할당 될 위치에 숨겨진 포인터 / 참조를 전달하고 함수는 그것을 사용하여 값이 끝나는 값을 만듭니다. C ++ 표준에는이를 허용하기위한 특수한 조항이 포함되어 있으므로 (예를 들어) 복사 생성자가 부작용을 보이는 경우에도 복사 생성자를 사용하여 값을 반환 할 필요는 없습니다. 예를 들면 다음과 같습니다.

#include <vector>
#include <numeric>
#include <iostream>
#include <stdlib.h>
#include <algorithm>
#include <iterator>

class X {
    std::vector<int> a;
public:
    X() {
        std::generate_n(std::back_inserter(a), 32767, ::rand);
    }

    X(X const &x) {
        a = x.a;
        std::cout << "Copy ctor invoked\n";
    }

    int sum() { return std::accumulate(a.begin(), a.end(), 0); }
};

X func() {
    return X();
}

int main() {
    X x = func();

    std::cout << "sum = " << x.sum();
    return 0;
};

여기서 기본 아이디어는 매우 간단합니다. 가능하면 복사하지 않는 충분한 내용의 클래스를 만듭니다 ( std::vector32767의 임의 정수로 채 웁니다). 우리는 그것이 언제 / 복사되는지 보여줄 명백한 사본 ctor를 가지고 있습니다. 또한 객체의 임의의 값으로 무언가를 수행하는 코드가 조금 더 있으므로 최적화 프로그램은 아무것도하지 않기 때문에 클래스에 대한 모든 것을 제거하지 않습니다.

그런 다음 함수에서 이러한 객체 중 하나를 반환하는 코드가 있고 합산을 사용하여 객체가 완전히 무시되는 것이 아니라 실제로 만들어 졌는지 확인합니다. 적어도 최신 / 현대 컴파일러로 실행할 때 우리가 작성한 복사 생성자가 전혀 실행되지 않는다는 것을 알았습니다. 그렇습니다. 빠른 복사조차도 shared_ptr복사하지 않는 것보다 느립니다. 조금도.

이사를하면 단순히 없이는 직접 할 수없는 많은 일을 할 수 있습니다. 외부 병합 정렬의 "병합"부분을 고려하십시오. 예를 들어 8 개의 파일을 병합 할 수 있습니다. 이상적으로 당신이로 그 파일을 모두 팔을 넣어 싶습니다 vector- 이후 있지만 vector(현재의 C ++ 03) 요소를 복사 할 수 있어야하고, ifstreams는 일부 나왔습니다 붙어, 복사 할 수 없습니다 unique_ptr/ shared_ptr, 벡터에 넣을 수 있도록 참고 심지어 우리의 (예를 들어)의 경우 reserve공간 vector우리가 반드시 우리의 것, 그래서 ifstream코드가 비록 컴파일되지 않도록, s는 정말 복사되지 않습니다 컴파일러는 것을 알 수 없습니다 우리는 복사 생성자가 없을 것 알고 어쨌든 사용.

여전히 복사 할 수는 없지만 C ++ 11에서는 ifstream 을 이동할 수 있습니다. 이 경우 객체 는 움직일 수 없지만 필요한 경우 컴파일러를 행복하게 유지하여 스마트 포인터 해킹없이 ifstream객체를 vector직접 넣을 수 있습니다 .

벡터 수행 확장은 이동의 의미가 정말 할 수있는 시간이 꽤 괜찮은 예이다 / 유용하지만입니다. 이 경우 RVO / NRVO는 도움이되지 않을 것입니다. 함수의 리턴 값을 다루지 않기 때문입니다. 우리는 하나의 객체를 가지고있는 하나의 벡터를 가지고 있으며, 그 객체를 새로운 큰 메모리 덩어리로 옮기고 싶습니다.

C ++ 03에서는 새 메모리에 객체의 사본을 만든 다음 이전 메모리의 기존 객체를 삭제하여 수행했습니다. 그러나 모든 사본을 기존 사본을 버리는 것만으로도 많은 시간을 낭비했습니다. C ++ 11에서는 대신 이동할 수 있습니다. 이를 통해 일반적으로 (일반적으로 훨씬 느린) 딥 카피 대신 얕은 카피를 수행 할 수 있습니다. 다시 말해, 문자열이나 벡터 (두 개의 예제에만 해당)를 사용하면 포인터가 참조하는 모든 데이터를 복사하는 대신 객체에 포인터를 복사하면됩니다.


매우 자세한 설명에 감사드립니다. 올바르게 이해하면 이동 하는 모든 상황 이 정상적인 포인터로 처리 될 수 있지만 매번 저글링하는 모든 포인터를 프로그래밍하는 것은 안전하지 않습니다 (복잡하고 오류가 발생하기 쉽습니다). 따라서 대신 후드 아래에 unique_ptr (또는 유사한 메커니즘)이 있으며 이동 의미는 하루가 끝나면 포인터 복사 만 있고 객체 복사는하지 않도록합니다.
— Giorgio

@Giorgio : 그렇습니다. 언어는 실제로 이동 의미론을 추가하지 않습니다. rvalue 참조를 추가합니다. rvalue 참조 (분명히 충분 함)는 rvalue에 바인딩 될 수 있습니다.이 경우 데이터의 내부 표현을 "훔쳐서"깊은 복사를 수행하는 대신 포인터를 복사하는 것이 안전하다는 것을 알고 있습니다.
— Jerry Coffin 2016 년

4

치다:

vector<string> v;

v에 문자열을 추가하면 필요에 따라 확장되고 각 재 할당마다 문자열을 복사해야합니다. 이동 생성자를 사용하면 기본적으로 문제가 아닙니다.

물론 다음과 같은 작업을 수행 할 수도 있습니다.

vector<unique_ptr<string>> v;

그러나 std::unique_ptr이동 생성자를 구현 하기 때문에 잘 작동합니다 .

std::shared_ptr실제로 소유권을 공유 한 (드문) 상황에서만 사용 하는 것이 좋습니다.


그러나 대신 30 개의 데이터 멤버 string가있는 인스턴스가 Foo있다면 어떨까요? unique_ptr버전은보다 효율적으로되지 않을 것?
— Vassilis

2

반환 값은 일종의 참조 대신 값으로 전달하려는 경우가 가장 많습니다. 큰 성능 저하없이 '스택에서'객체를 신속하게 반환 할 수 있다면 좋을 것입니다. 반면에,이 문제를 해결하는 것은 특히 어렵지 않습니다 (공유 포인터는 사용하기가 너무 쉽습니다 ...).이를 수행하기 위해 객체에 대해 추가 작업을 수행 할 가치가 있는지 확실하지 않습니다.


또한 일반적으로 스마트 포인터를 사용하여 함수 / 메소드에서 반환되는 객체를 래핑합니다.
— 조르지오

1
@Giorgio : 확실히 난독하고 느립니다.
— DeadMG

간단한 스택 객체를 반환하면 최신 컴파일러가 자동 이동을 수행해야하므로 공유 ptr 등이 필요하지 않습니다.
— Christian Severin
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.