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.