etc./StackOverFlow

이동 의미론이란 무엇입니까?

청렴결백한 만능 재주꾼 2021. 12. 20. 10:11
반응형

질문자 :dicroce


저는 C++0x 에 관한 Scott Meyers와의 소프트웨어 엔지니어링 라디오 팟캐스트 인터뷰를 방금 들었습니다. 대부분의 새로운 기능은 나에게 의미가 있었고, 지금은 C++0x에 대해 하나를 제외하고 정말 흥분됩니다. 나는 여전히 이동 의미를 이해하지 못합니다 ... 정확히 무엇입니까?



예제 코드로 이동 의미를 이해하는 것이 가장 쉽습니다. 힙 할당 메모리 블록에 대한 포인터만 보유하는 매우 간단한 문자열 클래스부터 시작하겠습니다.

 #include <cstring> #include <algorithm> class string { char* data; public: string(const char* p) { size_t size = std::strlen(p) + 1; data = new char[size]; std::memcpy(data, p, size); }

메모리를 직접 관리하기로 선택했으므로 3 의 규칙을 따라야 합니다. 할당 연산자 작성을 연기하고 지금은 소멸자와 복사 생성자만 구현하겠습니다.

 ~string() { delete[] data; } string(const string& that) { size_t size = std::strlen(that.data) + 1; data = new char[size]; std::memcpy(data, that.data, size); }

복사 생성자는 문자열 개체 복사의 의미를 정의합니다. 다음 예에서 복사본을 만들 수 있는 문자열 유형의 모든 표현식에 바인딩하는 매개변수 const string& that

 string a(x); // Line 1 string b(x + y); // Line 2 string c(some_function_returning_a_string()); // Line 3

이제 이동 의미론에 대한 핵심 통찰력이 제공됩니다. x 를 복사하는 첫 번째 줄에서만 이 깊은 복사가 실제로 필요합니다. 왜냐하면 x x 가 어떻게든 변경되면 매우 놀랄 것이기 때문입니다. x 세 번(이 문장을 포함하면 네 번) 말하고 매번 똑같은 대상을 의미하는 것을 눈치채셨나요? x 와 같은 표현식을 "lvalue"라고 부릅니다.

2행과 3행의 인수는 lvalue가 아니라 rvalue입니다. 기본 문자열 객체에는 이름이 없으므로 클라이언트가 나중에 다시 검사할 방법이 없기 때문입니다. rvalue는 다음 세미콜론에서 소멸되는 임시 객체를 나타냅니다(더 정확하게는 rvalue를 사전적으로 포함하는 전체 표현식의 끝에서). bc 초기화하는 동안 소스 문자열로 원하는 모든 작업을 수행할 수 있고 클라이언트가 차이점을 알 수 없기 때문에 중요합니다!

C++0x는 무엇보다도 함수 오버로딩을 통해 rvalue 인수를 감지할 수 있는 "rvalue 참조"라는 새로운 메커니즘을 도입했습니다. 우리가 해야 할 일은 rvalue 참조 매개변수가 있는 생성자를 작성하는 것입니다. 그 생성자 내부에서 우리는 우리가 어떤 유효한 상태로두고만큼 우리가 소스와 함께 원하는 모든 것을 할 수 있습니다 :

 string(string&& that) // string&& is an rvalue reference to a string { data = that.data; that.data = nullptr; }

우리가 여기서 무엇을 했습니까? 힙 데이터를 깊이 복사하는 대신 포인터를 복사한 다음 원래 포인터를 null로 설정했습니다(소스 개체의 소멸자에서 'delete[]'가 '방금 훔친 데이터'를 해제하는 것을 방지하기 위해). 사실상, 우리는 원래 소스 문자열에 속한 데이터를 "도용"했습니다. 다시 말하지만, 핵심 통찰력은 어떤 상황에서도 클라이언트가 소스가 수정되었음을 감지할 수 없다는 것입니다. 여기서는 실제로 복사를 수행하지 않으므로 이 생성자를 "이동 생성자"라고 합니다. 그 작업은 리소스를 복사하는 대신 한 개체에서 다른 개체로 이동하는 것입니다.

축하합니다. 이제 이동 의미론의 기본 사항을 이해했습니다! 계속해서 할당 연산자를 구현해 보겠습니다. copy and swap 관용구에 익숙하지 않다면 배우고 돌아와서 예외 안전과 관련된 멋진 C++ 관용구입니다.

 string& operator=(string that) { std::swap(data, that.data); return *this; } };

어, 그게 다야? "rvalue 참조는 어디에 있습니까?" 물을 수도 있습니다. "여기서는 필요없어!" 내 대답입니다 :)

우리는 매개 변수를 전달합니다 that 값으로, 그래서 that 가 다른 모든 문자열 객체처럼 초기화 할 수 있습니다. 정확히 어떻게되어 that 초기화 될 것? C++98 의 옛날에는 대답이 "복사 생성자에 의해"였을 것입니다. C++0x에서 컴파일러는 할당 연산자에 대한 인수가 lvalue인지 rvalue인지에 따라 복사 생성자와 이동 생성자 사이에서 선택합니다.

따라서 a = b 라고 하면 복사 생성자 가 이를 초기화 that (표현식 b 는 lvalue이기 때문에) 할당 연산자는 내용을 새로 생성된 전체 복사본으로 바꿉니다. 이것이 바로 복사 및 교환 관용구의 정의입니다. 복사본을 만들고, 내용을 복사본과 교환한 다음, 범위를 벗어나 복사본을 제거합니다. 여기에 새로운 것은 없습니다.

a = x + y 라고 말하면 이동 생성자 는 이를 초기화 that (표현식 x + y 가 rvalue이기 때문에) 깊은 복사가 필요하지 않고 효율적인 이동만 있습니다. that 여전히 인수로부터 독립적인 객체이지만 힙 데이터를 복사할 필요가 없고 이동만 하면 되기 때문에 구성이 간단했습니다. x + y 는 rvalue이기 때문에 복사할 필요가 없었고, 다시 rvalue로 표시된 문자열 객체에서 이동해도 괜찮습니다.

요약하자면, 복사 생성자는 원본을 그대로 유지해야 하기 때문에 깊은 복사를 만듭니다. 반면에 이동 생성자는 포인터를 복사한 다음 소스의 포인터를 null로 설정할 수 있습니다. 클라이언트가 개체를 다시 검사할 방법이 없기 때문에 이러한 방식으로 소스 개체를 "무효화"해도 됩니다.

이 예가 요점을 파악했으면 합니다. 참조를 rvalue하고 의미 체계를 이동하는 데에는 더 많은 것이 있으며 단순하게 유지하기 위해 의도적으로 생략했습니다. 자세한 내용을 원하시면 제 보충 답변을 참조하십시오.


fredoverflow

내 첫 번째 대답은 이동 의미론에 대한 매우 단순화된 소개였으며 단순하게 유지하기 위해 의도적으로 많은 세부 사항을 생략했습니다. 하지만 시맨틱을 움직여야 할 부분이 더 많고, 그 공백을 메우기 위한 2차 답변이 필요한 시점이라고 생각했다. 첫 번째 답변은 이미 꽤 오래된 것이며, 단순히 완전히 다른 텍스트로 대체하는 것은 옳지 않다고 생각합니다. 아직까지는 첫 입문서로 좋은 역할을 하고 있다고 생각합니다. 그러나 더 깊이 파고 싶다면 계속 읽으십시오 :)

Stephan T. Lavavej는 귀중한 피드백을 제공하는 시간을 가졌습니다. 정말 고마워요, 스테판!

소개

이동 의미를 사용하면 특정 조건에서 개체가 다른 개체의 외부 리소스에 대한 소유권을 가질 수 있습니다. 이것은 두 가지 면에서 중요합니다.

  1. 값비싼 사본을 값싼 움직임으로 바꿉니다. 예를 보려면 내 첫 번째 답변을 참조하십시오. 개체가 하나 이상의 외부 리소스를 관리하지 않는 경우(직접 또는 해당 구성원 개체를 통해 간접적으로) 이동 의미 체계는 복사 의미 체계에 비해 이점을 제공하지 않습니다. 이 경우 개체를 복사하는 것과 개체를 이동하는 것은 정확히 같은 의미입니다.

     class cannot_benefit_from_move_semantics { int a; // moving an int means copying an int float b; // moving a float means copying a float double c; // moving a double means copying a double char d[64]; // moving a char array means copying a char array // ... };
  2. 안전한 "이동 전용" 유형 구현 즉, 복사는 의미가 없지만 이동은 의미가 있는 유형입니다. 예를 들면 잠금, 파일 핸들, 고유 소유권 의미를 가진 스마트 포인터가 있습니다. 참고: 이 답변은 C++11에서 std::unique_ptr 로 대체된 더 이상 사용되지 않는 C++98 표준 라이브러리 템플릿인 std::auto_ptr 중급 C++ 프로그래머는 아마도 std::auto_ptr 어느 정도 익숙할 것이며, 표시되는 "이동 의미론" 때문에 C++11에서 이동 의미론을 논의하기 위한 좋은 출발점인 것 같습니다. YMMV.

움직임이란 무엇입니까?

std::auto_ptr<T> 라는 고유 소유권 의미 체계를 가진 스마트 포인터를 제공합니다. auto_ptr 에 익숙하지 않은 경우 그 목적은 예외가 발생하더라도 동적으로 할당된 객체가 항상 해제되도록 보장하는 것입니다.

 { std::auto_ptr<Shape> a(new Triangle); // ... // arbitrary code, could throw exceptions // ... } // <--- when a goes out of scope, the triangle is deleted automatically

auto_ptr 의 특이한 점은 "복사" 동작입니다.

 auto_ptr<Shape> a(new Triangle); +---------------+ | triangle data | +---------------+ ^ | | | +-----|---+ | +-|-+ | a | p | | | | | +---+ | +---------+ auto_ptr<Shape> b(a); +---------------+ | triangle data | +---------------+ ^ | +----------------------+ | +---------+ +-----|---+ | +---+ | | +-|-+ | a | p | | | b | p | | | | | +---+ | | +---+ | +---------+ +---------+

의 초기화 방법을 참고 ba 대신 삼각형을 복사하지 않지만,은에서 삼각형의 소유권 전송 하는 a b . 우리는 또한 "라고 a 로 이동 b "는 삼각형에서 이동 "또는 a b ". 삼각형 자체가 항상 메모리의 같은 위치에 있기 때문에 혼란스럽게 들릴 수 있습니다.

개체를 이동한다는 것은 개체가 관리하는 일부 리소스의 소유권을 다른 개체로 이전하는 것을 의미합니다.

auto_ptr 의 복사 생성자는 아마도 다음과 같이 보일 것입니다(다소 단순화됨):

 auto_ptr(auto_ptr& source) // note the missing const { p = source.p; source.p = 0; // now the source no longer owns the object }

위험하고 무해한 움직임

auto_ptr 의 위험한 점은 구문상 사본처럼 보이는 것이 실제로는 이동이라는 것입니다. auto_ptr 에서 멤버 함수를 호출하려고 하면 정의되지 않은 동작이 호출되므로 다음에서 이동한 후 auto_ptr

 auto_ptr<Shape> a(new Triangle); // create triangle auto_ptr<Shape> b(a); // move a into b double area = a->area(); // undefined behavior

그러나 auto_ptr 이 항상 위험한 것은 아닙니다. auto_ptr 대한 완벽한 사용 사례입니다.

 auto_ptr<Shape> make_triangle() { return auto_ptr<Shape>(new Triangle); } auto_ptr<Shape> c(make_triangle()); // move temporary into c double area = make_triangle()->area(); // perfectly safe

두 예제가 어떻게 동일한 구문 패턴을 따르는지 확인하십시오.

 auto_ptr<Shape> variable(expression); double area = expression->area();

그러나 그 중 하나는 정의되지 않은 동작을 호출하지만 다른 하나는 그렇지 않습니다. 그래서 표현의 차이는 무엇 과 a make_triangle() ? 둘 다 같은 유형 아닌가요? 실제로 그들은 서로 다른 가치 범주 를 가지고 있습니다.

가치 카테고리

물론, 발현 사이에 약간 깊은 차이가 있어야 나타낸다 a auto_ptr 변수 및 발현 make_triangle() 되돌아가있는 함수 호출이다 auto_ptr 따라서 새로운 임시 생성 값에 의해, auto_ptr 개체마다 시간이 호출을 . a 는 lvalue 의 예이고 make_triangle() 은 rvalue 의 예입니다.

같은 lvalues에서 이동 a 우리가 나중에 통해 멤버 함수를 호출 할 수 있기 때문에, 위험 정의되지 않은 동작을 호출. a make_triangle() 과 같은 rvalue에서 이동하는 것은 완벽하게 안전합니다. 복사 생성자가 작업을 완료한 후에는 임시 값을 다시 사용할 수 없기 때문입니다. 해당 임시를 나타내는 표현은 없습니다. 단순히 make_triangle() 다시 작성하면 다른 임시 값을 얻습니다. 사실, 이동된 임시는 이미 다음 줄에서 사라졌습니다.

 auto_ptr<Shape> c(make_triangle()); ^ the moved-from temporary dies right here

문자 lr 은 할당의 왼쪽과 오른쪽에 역사적 기원이 있습니다. 할당의 왼쪽에 나타날 수 없는 lvalue(할당 연산자가 없는 배열 또는 사용자 정의 유형과 같은)가 있고 할 수 있는 rvalue(클래스 유형의 모든 rvalue)가 있기 때문에 C++에서는 더 이상 사실이 아닙니다. 할당 연산자).

클래스 유형의 rvalue는 평가가 임시 객체를 생성하는 표현식입니다. 정상적인 상황에서 동일한 범위 내의 다른 식은 동일한 임시 개체를 나타내지 않습니다.

R값 참조

이제 lvalue에서 이동하는 것은 잠재적으로 위험하지만 rvalue에서 이동하는 것은 무해하다는 것을 이해합니다. C++에 lvalue 인수와 rvalue 인수를 구별하는 언어 지원이 있었다면 lvalue에서 이동하는 것을 완전히 금지하거나 적어도 호출 사이트에서 lvalue에서 명시적으로 이동하여 더 이상 우연히 이동하지 않도록 할 수 있습니다.

이 문제에 대한 C++11의 대답은 rvalue 참조 입니다. rvalue 참조는 rvalue에만 바인딩되는 새로운 종류의 참조이며 구문은 X&& 입니다. 좋은 오래된 참조 X& 는 이제 lvalue 참조 로 알려져 있습니다. (참고 X&& 참조에 대한 참조 아니다. 그런 일이 C에 없다가 ++)

const 를 믹스에 넣으면 이미 4가지 다른 종류의 참조가 있습니다. 어떤 종류의 X 유형 표현식에 바인딩할 수 있습니까?

 lvalue const lvalue rvalue const rvalue --------------------------------------------------------- X& yes const X& yes yes yes yes X&& yes const X&& yes yes

const X&& 는 잊어도 됩니다. rvalue에서 읽기로 제한하는 것은 그다지 유용하지 않습니다.

rvalue 참조 X&& 는 rvalue에만 바인딩되는 새로운 종류의 참조입니다.

암시적 변환

Rvalue 참조는 여러 버전을 거쳤습니다. 버전 2.1부터 rvalue 참조 X&& Y 에서 X 로의 암시적 변환이 있는 경우 Y 의 모든 값 범주에도 바인딩됩니다. X 유형의 임시가 생성되고 rvalue 참조가 해당 임시에 바인딩됩니다.

 void some_function(std::string&& r); some_function("hello world");

위의 예에서 "hello world" const char[12] 유형의 lvalue입니다. 이 암시 적으로 변환하기 때문에 const char[12] 내지 const char* 하는 std::string 입력의 임시 std::string 생성되고, 그리고 r 그 임시로 결합된다. 이것은 rvalue(표현식)와 임시(객체)의 구분이 약간 모호한 경우 중 하나입니다.

이동 생성자

X&& 매개변수가 있는 함수의 유용한 예 는 이동 생성자 X::X(X&& source) 입니다. 그 목적은 관리 자원의 소유권을 소스에서 현재 개체로 이전하는 것입니다.

C++11에서 std::auto_ptr<T> 는 rvalue 참조를 활용하는 std::unique_ptr<T> 로 대체되었습니다. unique_ptr 의 단순화된 버전을 개발하고 논의할 것입니다. 먼저 원시 포인터를 캡슐화하고 연산자 ->* 오버로드하여 클래스가 포인터처럼 느껴지도록 합니다.

 template<typename T> class unique_ptr { T* ptr; public: T* operator->() const { return ptr; } T& operator*() const { return *ptr; }

생성자는 객체의 소유권을 가져오고 소멸자는 객체를 삭제합니다.

 explicit unique_ptr(T* p = nullptr) { ptr = p; } ~unique_ptr() { delete ptr; }

이제 흥미로운 부분인 이동 생성자가 나옵니다.

 unique_ptr(unique_ptr&& source) // note the rvalue reference { ptr = source.ptr; source.ptr = nullptr; }

auto_ptr 복사 생성자가 하는 일을 정확히 수행하지만 rvalue로만 제공할 수 있습니다.

 unique_ptr<Shape> a(new Triangle); unique_ptr<Shape> b(a); // error unique_ptr<Shape> c(make_triangle()); // okay

a 가 lvalue이기 때문에 두 번째 줄은 컴파일에 실패 unique_ptr&& source 매개변수는 rvalue에만 바인딩될 수 있습니다. 이것이 바로 우리가 원했던 것입니다. 위험한 움직임은 절대 암시적이어서는 안 됩니다. make_triangle() 이 rvalue이기 때문에 세 번째 줄은 잘 컴파일됩니다. 이동 생성자는 소유권을 임시에서 c 합니다. 다시 말하지만 이것이 바로 우리가 원했던 것입니다.

이동 생성자는 관리 자원의 소유권을 현재 개체로 전송합니다.

할당 연산자 이동

마지막으로 누락된 부분은 이동 할당 연산자입니다. 그 역할은 이전 리소스를 해제하고 인수에서 새 리소스를 얻는 것입니다.

 unique_ptr& operator=(unique_ptr&& source) // note the rvalue reference { if (this != &source) // beware of self-assignment { delete ptr; // release the old resource ptr = source.ptr; // acquire the new resource source.ptr = nullptr; } return *this; } };

이 이동 할당 연산자 구현이 소멸자와 이동 생성자의 논리를 복제하는 방법에 유의하십시오. copy-and-swap 관용구에 대해 알고 있습니까? move-and-swap 관용구로 이동 의미론에 적용될 수도 있습니다.

 unique_ptr& operator=(unique_ptr source) // note the missing reference { std::swap(ptr, source.ptr); return *this; } };

이제 source unique_ptr 유형의 변수이므로 이동 생성자에 의해 초기화됩니다. 즉, 인수가 매개변수로 이동됩니다. 이동 생성자 자체에 rvalue 참조 매개변수가 있기 때문에 인수는 여전히 rvalue여야 합니다. operator= 닫는 중괄호에 도달하면 source 가 범위를 벗어나 이전 리소스를 자동으로 해제합니다.

이동 할당 연산자는 관리 리소스의 소유권을 현재 개체로 이전하여 이전 리소스를 해제합니다. move-and-swap 관용구는 구현을 단순화합니다.

lvalue에서 이동

때때로 우리는 lvalue에서 이동하고 싶습니다. 즉, 때로는 컴파일러가 lvalue를 rvalue인 것처럼 처리하여 잠재적으로 안전하지 않더라도 이동 생성자를 호출할 수 있기를 원합니다. <utility> 헤더 내부에 std::move 라는 표준 라이브러리 함수 템플릿을 제공합니다. std::move 단순히 lvalue를 rvalue로 캐스팅하기 때문에 이 이름은 약간 불행합니다. 그 자체로는 아무것도 움직이지 않습니다. 그것은 단지 이동을 가능하게 합니다. 이름이 std::cast_to_rvalue 또는 std::enable_move 여야 할 수도 있지만 지금은 이름이 막혀 있습니다.

다음은 lvalue에서 명시적으로 이동하는 방법입니다.

 unique_ptr<Shape> a(new Triangle); unique_ptr<Shape> b(a); // still an error unique_ptr<Shape> c(std::move(a)); // okay

세 번째 줄 이후 유의하십시오 a 더 이상 삼각형을 소유하고있다. 그 때문에 명시 적으로 작성하여, 괜찮아 std::move(a) , 우리는 분명 우리의 의도를 만든 : 당신이 원하는 무엇이든 할 "친애하는 생성자 초기화하기 위해 a c , 나는 걱정하지 않는다 더 이상 가지고 주시기 바랍니다. a 와 당신의 방법 . " a

std::move(some_lvalue) 는 lvalue를 rvalue로 캐스팅하여 후속 이동을 가능하게 합니다.

X값

std::move(a) 가 rvalue이지만 평가 시 임시 객체가 생성 되지 않습니다. 이 수수께끼로 인해 위원회는 세 번째 가치 범주를 도입했습니다. 전통적인 의미의 rvalue는 아니지만 rvalue 참조에 바인딩될 수 있는 것을 xvalue (eXpiring 값)라고 합니다. 기존 rvalue는 prvalue (순수 rvalue)로 이름이 변경되었습니다.

prvalue와 xvalue는 모두 rvalue입니다. Xvalue와 lvalue는 모두 glvalue (일반화된 lvalue)입니다. 다이어그램을 사용하면 관계를 더 쉽게 파악할 수 있습니다.

 expressions / \ / \ / \ glvalues rvalues / \ / \ / \ / \ / \ / \ lvalues xvalues prvalues

xvalue만 실제로 새롭습니다. 나머지는 이름 변경 및 그룹화로 인한 것입니다.

C++98 rvalue는 C++11에서 prvalue로 알려져 있습니다. 정신적으로 앞 단락에서 "rvalue"의 모든 항목을 "prvalue"로 대체하십시오.

기능 밖으로 이동

지금까지 지역 변수와 함수 매개변수로의 이동을 보았습니다. 그러나 반대 방향으로도 이동이 가능합니다. 함수가 값으로 반환되면 호출 사이트의 일부 개체(지역 변수 또는 임시이지만 모든 종류의 개체일 수 있음)는 이동 생성자에 대한 인수로 return

 unique_ptr<Shape> make_triangle() { return unique_ptr<Shape>(new Triangle); } \-----------------------------/ | | temporary is moved into c | v unique_ptr<Shape> c(make_triangle());

static 선언되지 않은 지역 변수)도 암시적 으로 함수 밖으로 이동할 수 있습니다.

 unique_ptr<Shape> make_square() { unique_ptr<Shape> result(new Square); return result; // note the missing std::move }

이동 생성자가 lvalue result 를 인수로 받아들이는 이유는 무엇입니까? result 범위가 곧 종료되며 스택 해제 중에 소멸됩니다. result 가 어떻게 든 바뀌었다고 아무도 불평할 수 없었습니다. 제어 흐름이 호출자에게 돌아오면 result 는 더 이상 존재하지 않습니다! std::move 를 작성할 필요 없이 함수에서 자동 개체를 반환할 수 있는 특별한 규칙이 있습니다. 사실, 당신은 사용해서는std::move 이 억제로, 함수에서 (NRVO)의 "라는 이름의 반환 값 최적화"를 자동으로 객체를 이동합니다.

std::move 를 사용하여 자동 개체를 함수 밖으로 이동하지 마십시오.

두 팩토리 함수 모두에서 반환 유형은 rvalue 참조가 아닌 값입니다. Rvalue 참조는 여전히 참조이며 항상 그렇듯이 자동 개체에 대한 참조를 반환해서는 안 됩니다. 다음과 같이 컴파일러가 코드를 수락하도록 속이면 호출자는 댕글링 참조로 끝날 것입니다.

 unique_ptr<Shape>&& flawed_attempt() // DO NOT DO THIS! { unique_ptr<Shape> very_bad_idea(new Square); return std::move(very_bad_idea); // WRONG! }

rvalue 참조로 자동 개체를 반환하지 마십시오. std::move 아니라 이동 생성자에 의해 독점적으로 수행되며 rvalue를 rvalue 참조에 바인딩하는 것만이 아닙니다.

회원가입

조만간 다음과 같은 코드를 작성하게 될 것입니다.

 class Foo { unique_ptr<Shape> member; public: Foo(unique_ptr<Shape>&& parameter) : member(parameter) // error {} };

기본적으로 컴파일러는 parameter 가 lvalue라고 불평합니다. 유형을 보면 rvalue 참조를 볼 수 있지만 rvalue 참조는 단순히 "rvalue에 바인딩된 참조"를 의미합니다. 참조 자체가 rvalue임을 의미하지는 않습니다! 실제로 parameter 는 이름이 있는 일반 변수일 뿐입니다. parameter 는 생성자 본문 내에서 원하는 만큼 자주 사용할 수 있으며 항상 동일한 객체를 나타냅니다. 암묵적으로 그것으로부터 이동하는 것은 위험할 것이고, 따라서 언어는 그것을 금지합니다.

명명된 rvalue 참조는 다른 변수와 마찬가지로 lvalue입니다.

해결 방법은 수동으로 이동을 활성화하는 것입니다.

 class Foo { unique_ptr<Shape> member; public: Foo(unique_ptr<Shape>&& parameter) : member(std::move(parameter)) // note the std::move {} };

member parameter 가 더 이상 사용되지 않는다고 주장할 수 있습니다. 반환 값과 마찬가지로 std::move 를 자동으로 삽입하는 특별한 규칙이 없는 이유는 무엇입니까? 아마도 컴파일러 구현자에게 너무 많은 부담이 되기 때문일 것입니다. 예를 들어 생성자 본문이 다른 번역 단위에 있으면 어떻게 될까요? 대조적으로, 반환 값 규칙은 단순히 기호 테이블을 확인하여 return 키워드 뒤의 식별자가 자동 개체를 나타내는지 여부를 결정해야 합니다.

parameter 를 값으로 전달할 수도 있습니다. unique_ptr 과 같은 이동 전용 유형의 경우 아직 확립된 관용구가 없는 것 같습니다. 개인적으로 저는 값으로 전달하는 것을 선호합니다. 인터페이스가 덜 복잡해지기 때문입니다.

특별한 멤버 함수

C++98은 요청 시, 즉 어딘가에 필요할 때 복사 생성자, 복사 할당 연산자 및 소멸자라는 세 가지 특수 멤버 함수를 암시적으로 선언합니다.

 X::X(const X&); // copy constructor X& X::operator=(const X&); // copy assignment operator X::~X(); // destructor

Rvalue 참조는 여러 버전을 거쳤습니다. 버전 3.0부터 C++11은 필요에 따라 이동 생성자와 이동 할당 연산자라는 두 가지 추가 특수 멤버 함수를 선언합니다. VC10이나 VC11은 아직 버전 3.0을 따르지 않으므로 직접 구현해야 합니다.

 X::X(X&&); // move constructor X& X::operator=(X&&); // move assignment operator

이 두 개의 새로운 특수 멤버 함수는 특수 멤버 함수가 수동으로 선언되지 않은 경우에만 암시적으로 선언됩니다. 또한 고유한 이동 생성자 또는 이동 할당 연산자를 선언하면 복사 생성자나 복사 할당 연산자가 암시적으로 선언되지 않습니다.

이러한 규칙은 실제로 무엇을 의미합니까?

관리되지 않는 리소스 없이 클래스를 작성하면 5가지 특수 멤버 함수를 직접 선언할 필요가 없으며 올바른 복사 의미 체계와 이동 의미 체계를 무료로 얻을 수 있습니다. 그렇지 않으면 특수 멤버 함수를 직접 구현해야 합니다. 물론 클래스가 이동 의미론의 이점을 얻지 못하면 특수 이동 작업을 구현할 필요가 없습니다.

복사 할당 연산자와 이동 할당 연산자는 값으로 인수를 취하여 단일 통합 할당 연산자로 융합될 수 있습니다.

 X& X::operator=(X source) // unified assignment operator { swap(source); // see my first answer for an explanation return *this; }

이런 식으로 구현하는 특수 멤버 함수의 수가 5개에서 4개로 줄어듭니다. 예외 안전과 효율성 사이에는 절충점이 있지만 저는 이 문제에 대한 전문가가 아닙니다.

전달 참조( 이전 에는 범용 참조 라고 함)

다음 함수 템플릿을 고려하십시오.

 template<typename T> void foo(T&&);

언뜻 보기에는 rvalue 참조처럼 보이기 때문에 T&& 가 rvalue에만 바인딩될 것으로 예상할 수 있습니다. 하지만 T&& 는 lvalue에도 바인딩됩니다.

 foo(make_triangle()); // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&& unique_ptr<Shape> a(new Triangle); foo(a); // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&

인수가 입력 r- 수치의 경우 X , T 것으로 추론된다 X , 따라서 T&& 수단 X&& . 이것은 누구나 예상할 수 있는 것입니다. X 유형의 lvalue 인 경우 특별한 규칙으로 인해 TX& 추론되므로 T&& X& && 와 같은 것을 의미합니다. 그러나 C 이후 ++ 여전히 참조에 대한 참조의 아무 개념이없는, 타입 X& &&축소되어 X& . 이것은 처음에는 혼란스럽고 쓸모없게 들릴 수 있지만 참조 축소는 완벽한 전달을 위해 필수적입니다(여기서 논의되지 않음).

T&&는 rvalue 참조가 아니라 전달 참조입니다. 또한 lvalue에 바인딩되며, 이 경우 TT&& 는 모두 lvalue 참조입니다.

함수 템플릿을 rvalue 로 제한하려면 SFINAE를 유형 특성과 결합할 수 있습니다.

 #include <type_traits> template<typename T> typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type foo(T&&);

이동의 구현

이제 참조 축소를 이해 std::move 가 구현되는 방법은 다음과 같습니다.

 template<typename T> typename std::remove_reference<T>::type&& move(T&& t) { return static_cast<typename std::remove_reference<T>::type&&>(t); }

보시다시피 move T&& 덕분에 모든 종류의 매개변수를 허용하고 rvalue 참조를 반환합니다. std::remove_reference<T>::type 메타 함수 호출이 필요합니다. 그렇지 않으면 X 유형의 lvalue의 경우 반환 유형이 X& && X& 로 축소되기 때문입니다. 이후 t 항상 좌변입니다 (명명를 rvalue 참조가 좌변 있음을 유의)하지만, 우리가 바인딩 할 t 를 rvalue 참조에, 우리는 명시 적으로 캐스팅해야 t 올바른 리턴 유형으로. rvalue 참조를 반환하는 함수 호출 자체가 xvalue입니다. 이제 x값이 어디에서 왔는지 알 수 있습니다. ;)

std::move 와 같이 rvalue 참조를 반환하는 함수의 호출은 xvalue입니다.

이 예제에서는 rvalue 참조로 반환하는 t 는 자동 개체를 나타내지 않고 대신 호출자가 전달한 개체를 나타내기 때문입니다.


fredoverflow

실질적인 객체를 반환하는 함수가 있다고 가정합니다.

 Matrix multiply(const Matrix &a, const Matrix &b);

다음과 같은 코드를 작성할 때:

 Matrix r = multiply(a, b);

multiply() 결과에 대한 임시 개체를 만들고 복사 생성자를 호출하여 r 을 초기화한 다음 임시 반환 값을 소멸시킵니다. C++0x의 이동 의미 체계를 사용하면 "이동 생성자"를 호출하여 r 을 초기화한 다음 이를 파괴하지 않고 임시 값을 삭제할 수 있습니다.

Matrix 예제와 같이) 복사되는 객체가 내부 표현을 저장하기 위해 힙에 추가 메모리를 할당하는 경우에 특히 중요합니다. 복사 생성자는 내부 표현의 전체 복사본을 만들거나 참조 계산 및 쓰기 시 복사 의미 체계를 내부적으로 사용해야 합니다. 이동 생성자는 힙 메모리를 그대로 두고 Matrix 객체 내부에 포인터를 복사합니다.


Greg Hewgill

이동 의미 체계에 대한 훌륭하고 심층적인 설명에 정말로 관심이 있다면 이에 대한 원본 문서인 "A Proposal to Add Move Semantics Support to Add Move Semantics Support to C++ Language"를 적극 권장합니다.

매우 접근하기 쉽고 읽기 쉬우며 그들이 제공하는 이점에 대한 훌륭한 사례가 됩니다. WG21 웹사이트 에서 이동 의미론에 대한 다른 최신 최신 문서를 볼 수 있지만 이 문서는 최상위 수준의 관점에서 접근하고 세부적인 언어 세부 사항에 대해 많이 다루지 않기 때문에 아마도 가장 간단할 것입니다.


James McNellis

이동 의미 는 더 이상 소스 값이 필요하지 않을 때 리소스를 복사하는 것이 아니라 전송하는 것입니다.

C++03에서 객체는 종종 복사되지만 코드가 값을 다시 사용하기 전에 파괴되거나 할당됩니다. 예를 들어 RVO가 시작되지 않는 한 함수에서 값으로 반환하면 반환하는 값이 호출자의 스택 프레임에 복사된 다음 범위를 벗어나 소멸됩니다. 이것은 많은 예 중 하나일 뿐입니다. 소스 객체가 임시일 때 값에 의한 전달, 항목을 재정렬하는 sort capacity() 이 초과될 때 vector 재할당 등을 참조하십시오.

이러한 복사/파기 쌍이 비용이 많이 드는 것은 일반적으로 개체가 일부 무거운 리소스를 소유하기 때문입니다. 예를 들어 vector<string> string 개체의 배열을 포함하는 동적으로 할당된 메모리 블록을 소유할 수 있습니다. 이러한 객체를 복사하는 것은 비용이 많이 듭니다. 소스에서 동적으로 할당된 각 블록에 대해 새 메모리를 할당하고 모든 값을 복사해야 합니다. 그런 다음 방금 복사한 모든 메모리를 할당 해제해야 합니다. 그러나, 많은 이동 vector<string> 수단은 단지 목적지 (동적 메모리 블록을 참조) 약간의 포인터를 복사하고 소스들을 제로 아웃한다.


Dave Abrahams

쉬운(실용적인) 용어로:

개체를 복사한다는 것은 "정적" 멤버를 복사하고 동적 개체에 대해 new 오른쪽?

 class A { int i, *p; public: A(const A& a) : i(ai), p(new int(*ap)) {} ~A() { delete p; } };

그러나 객체를 이동 한다는 것은(실제적인 관점에서 반복합니다) 동적 객체의 포인터를 복사하는 것일 뿐 새 객체를 생성하는 것은 아닙니다.

하지만, 위험하지 않습니까? 물론 동적 개체를 두 번 파괴할 수 있습니다(세그먼테이션 오류). 따라서 이를 피하려면 소스 포인터를 두 번 파괴하지 않도록 "무효화"해야 합니다.

 class A { int i, *p; public: // Movement of an object inside a copy constructor. A(const A& a) : i(ai), p(ap) { ap = nullptr; // pointer invalidated. } ~A() { delete p; } // Deleting NULL, 0 or nullptr (address 0x0) is safe. };

좋아, 하지만 내가 개체를 움직이면 원본 개체가 쓸모 없게 되겠죠? 물론 특정 상황에서는 매우 유용합니다. 가장 분명한 것은 익명 객체(temporal, rvalue 객체, ..., 다른 이름으로 호출할 수 있음)를 사용하여 함수를 호출할 때입니다.

 void heavyFunction(HeavyType());

이 상황에서 익명 개체가 생성되고 다음에는 함수 매개변수에 복사되고 나중에 삭제됩니다. 따라서 익명 개체가 필요하지 않고 시간과 메모리를 절약할 수 있으므로 여기에서 개체를 이동하는 것이 좋습니다.

이것은 "rvalue" 참조의 개념으로 이어집니다. 수신된 객체가 익명인지 여부를 감지하기 위해서만 C++11에 존재합니다. = 연산자의 왼쪽 부분)라는 것을 이미 알고 있다고 생각하므로 lvalue로 작동할 수 있는 개체에 대한 명명된 참조가 필요합니다. rvalue는 정확히 반대이며 명명된 참조가 없는 객체입니다. 그렇기 때문에 익명 객체와 rvalue는 동의어입니다. 그래서:

 class A { int i, *p; public: // Copy A(const A& a) : i(ai), p(new int(*ap)) {} // Movement (&& means "rvalue reference to") A(A&& a) : i(ai), p(ap) { ap = nullptr; } ~A() { delete p; } };

이 경우 유형 A 의 개체를 "복사"해야 할 때 컴파일러는 전달된 개체의 이름 지정 여부에 따라 lvalue 참조 또는 rvalue 참조를 만듭니다. 그렇지 않은 경우 이동 생성자가 호출되고 개체가 임시임을 알고 있으며 동적 개체를 복사하는 대신 이동할 수 있으므로 공간과 메모리를 절약할 수 있습니다.

"정적" 개체는 항상 복사된다는 점을 기억하는 것이 중요합니다. 정적 개체(힙이 아닌 스택의 개체)를 "이동"하는 방법은 없습니다. 따라서 개체에 동적 구성원이 없는 경우(직접 또는 간접적으로) "이동"/ "복사"의 구분은 관련이 없습니다.

객체가 복잡하고 소멸자가 라이브러리의 함수 호출, 다른 전역 함수 호출 등과 같은 다른 2차 효과를 갖는 경우 플래그로 이동 신호를 보내는 것이 더 나을 수 있습니다.

 class Heavy { bool b_moved; // staff public: A(const A& a) { /* definition */ } A(A&& a) : // initialization list { a.b_moved = true; } ~A() { if (!b_moved) /* destruct object */ } };

따라서 코드가 더 짧고(각 동적 멤버에 대해 nullptr

다른 일반적인 질문: A&&const A&& 의 차이점은 무엇입니까? 물론 첫 번째 경우에는 개체를 수정할 수 있고 두 번째 경우에는 수정할 수 없지만 실질적인 의미는 무엇입니까? 두 번째 경우에는 수정할 수 없으므로 개체를 무효화할 방법이 없으며(변경 가능한 플래그 또는 이와 유사한 것을 제외하고) 복사 생성자와 실질적인 차이가 없습니다.

그리고 완벽한 포워딩 이란 무엇입니까? "rvalue 참조"가 "호출자의 범위"에 있는 명명된 개체에 대한 참조라는 것을 아는 것이 중요합니다. 그러나 실제 범위에서 rvalue 참조는 객체에 대한 이름이므로 명명된 객체로 작동합니다. 다른 함수에 대한 rvalue 참조를 전달하면 명명된 객체를 전달하는 것이므로 객체는 임시 객체처럼 수신되지 않습니다.

 void some_function(A&& a) { other_function(a); }

객체 a other_function 의 실제 매개변수에 복사됩니다. 객체 a 임시 객체로 계속 처리하려면 std::move 함수를 사용해야 합니다.

 other_function(std::move(a));

이 줄에서 std::movea 를 rvalue로 other_function 은 이름 없는 객체로 객체를 받습니다. 물론 other_function 이 명명되지 않은 개체와 함께 작동하도록 특정 오버로딩이 없는 경우 이 구분은 중요하지 않습니다.

완벽한 전달인가요? 아니지만 우리는 매우 가깝습니다. 완벽한 전달은 다음과 같은 목적으로 템플릿 작업에만 유용합니다. 객체를 다른 함수에 전달해야 하는 경우 명명된 객체를 수신하면 해당 객체가 명명된 객체로 전달되고 그렇지 않은 경우, 이름 없는 객체처럼 전달하고 싶습니다.

 template<typename T> void some_function(T&& a) { other_function(std::forward<T>(a)); }

std::forward 통해 C++11에서 구현된 완벽한 전달을 사용하는 프로토타입 함수의 서명입니다. 이 함수는 템플릿 인스턴스화의 몇 가지 규칙을 이용합니다.

 `A& && == A&` `A&& && == A&&`

따라서, 만일 T 에 좌변 기준이다 (T = A &) 또한 (A & && => A &가). A a T A 대한 rvalue 참조인 경우 a 도 (A&& && => A&&)입니다. 두 경우 모두에서, a 실제의 범위에서 지정된 오브젝트이지만, T 뷰의 호출자 범위의 관점에서 그것의 "참조 유형"의 정보를 포함한다. 이 정보( T )는 템플릿 매개변수로 forward T 의 유형에 따라 'a'가 이동되거나 이동되지 않습니다.


Peregring-lk

복사 의미론과 비슷하지만 모든 데이터를 복제하는 대신 "이동"되는 개체에서 데이터를 훔칩니다.


Terry Mahaffey

복사 의미가 무엇을 의미하는지 알고 있습니까? 이는 복사 가능한 유형이 있음을 의미합니다. 사용자 정의 유형의 경우 복사 생성자 및 할당 연산자를 명시적으로 작성하거나 컴파일러가 암시적으로 생성하는 것을 정의합니다. 이것은 복사를 할 것입니다.

이동 의미 체계는 기본적으로 r-값 참조(&&(2개의 앰퍼샌드)를 사용하는 새로운 참조 유형)를 사용하는 생성자가 있는 사용자 정의 유형입니다. 이는 비 const입니다. 이를 이동 생성자라고 하며 할당 연산자도 마찬가지입니다. 따라서 이동 생성자는 무엇을 합니까? 소스 인수에서 메모리를 복사하는 대신 소스에서 대상으로 메모리를 '이동'합니다.

언제 그렇게 하시겠습니까? 음 std::vector가 예입니다. 임시 std::vector를 만들고 함수에서 반환한다고 가정해 보겠습니다.

 std::vector<foo> get_foos();

함수가 반환될 때 복사 생성자에서 오버헤드가 발생합니다. std::vector에 복사하는 대신 이동 생성자가 있으면 포인터를 설정하고 동적으로 '이동'할 수 있습니다. 새 인스턴스에 대한 메모리. std::auto_ptr을 사용한 소유권 이전 의미와 같습니다.


snk_kid

이동 의미 체계의 필요성을 설명하기 위해 이동 의미 체계가 없는 다음 예를 살펴보겠습니다.

T 유형의 객체를 취하고 T 의 객체를 반환하는 함수입니다.

 T f(T o) { return o; } //^^^ new object constructed

상기 기능 이용이 함수가 호출 될 때 목적 함수에 의해 사용될 수 있도록 구성되어야한다는 의미있는 값으로 부른다.
함수도 value 로 반환 하기 때문에 반환 값에 대해 또 다른 새 객체가 생성됩니다.

 T b = f(a); //^ new object constructed

두 개의 새 개체가 생성되었으며 그 중 하나는 함수 기간 동안에만 사용되는 임시 개체입니다.

반환 값에서 새 객체가 생성되면 임시 객체의 내용을 새 객체 에 복사하기 위해 복사 생성자가 호출됩니다. b. 함수가 완료된 후 함수에 사용된 임시 객체는 범위를 벗어나 소멸됩니다.


이제 복사 생성자 가 하는 일을 살펴보겠습니다.

먼저 개체를 초기화한 다음 이전 개체의 모든 관련 데이터를 새 개체로 복사해야 합니다.
클래스에 따라 데이터가 매우 많은 컨테이너일 수 있으므로 많은 시간메모리 사용량을 나타낼 수 있습니다.

 // Copy constructor T::T(T &old) { copy_data(m_a, old.m_a); copy_data(m_b, old.m_b); copy_data(m_c, old.m_c); }

이동 의미 체계를 사용하면 복사보다 단순히 데이터를 이동 하여 이 작업의 대부분을 덜 불편하게 만들 수 있습니다.

 // Move constructor T::T(T &&old) noexcept { m_a = std::move(old.m_a); m_b = std::move(old.m_b); m_c = std::move(old.m_c); }

데이터 이동에는 데이터를 새 개체와 다시 연결하는 작업이 포함됩니다. 그리고 복사가 전혀 발생하지 않습니다 .

rvalue 참조로 수행됩니다.
rvalue 참조는 한 가지 중요한 차이점이 lvalue 참조와 거의 유사하게 작동합니다.
rvalue 참조는 이동할 수 있고 lvalue 는 이동할 수 없습니다.

cppreference.com에서 :

강력한 예외 보장을 가능하게 하려면 사용자 정의 이동 생성자가 예외를 throw하지 않아야 합니다. 사실, 표준 컨테이너는 일반적으로 컨테이너 요소를 재배치해야 할 때 이동과 복사 중에서 선택하기 위해 std::move_if_noexcept에 의존합니다. 복사 및 이동 생성자가 모두 제공되는 경우 인수가 rvalue(이름 없는 임시와 같은 prvalue 또는 std::move의 결과와 같은 xvalue)인 경우 오버로드 확인은 이동 생성자를 선택하고 다음과 같은 경우 복사 생성자를 선택합니다. 인수는 lvalue(명명된 객체 또는 lvalue 참조를 반환하는 함수/연산자)입니다. 복사 생성자만 제공되면 모든 인수 범주가 이를 선택합니다(rvalue가 const 참조에 바인딩될 수 있기 때문에 const에 대한 참조가 필요한 한). 그러면 이동을 사용할 수 없을 때 이동을 위한 대체 복사가 만들어집니다. 많은 상황에서 이동 생성자는 관찰 가능한 부작용을 생성하더라도 최적화됩니다. 복사 제거를 참조하십시오. 생성자는 rvalue 참조를 매개변수로 사용할 때 '이동 생성자'라고 합니다. 아무것도 이동할 의무가 없으며 클래스는 이동할 리소스가 필요하지 않으며 '이동 생성자'는 매개변수가 const rvalue 참조(const T&&).


Andreas DM

제가 제대로 이해하고 있는지 확인하기 위해 이 글을 씁니다.

이동 의미 체계는 큰 개체의 불필요한 복사를 방지하기 위해 만들어졌습니다. Bjarne Stroustrup은 그의 저서 "The C++ Programming Language"에서 기본적으로 불필요한 복사가 발생하는 두 가지 예를 사용합니다. 하나는 두 개의 큰 개체를 교환하는 것이고 다른 하나는 메서드에서 큰 개체를 반환하는 것입니다.

두 개의 큰 개체를 교환하려면 일반적으로 첫 번째 개체를 임시 개체에 복사하고, 두 번째 개체를 첫 번째 개체에 복사하고, 임시 개체를 두 번째 개체에 복사하는 작업을 포함합니다. 기본 제공 유형의 경우 이는 매우 빠르지만 큰 개체의 경우 이 세 개의 복사본에 많은 시간이 걸릴 수 있습니다. "이동 할당"을 통해 프로그래머는 기본 복사 동작을 무시하고 대신 개체에 대한 참조를 교환할 수 있습니다. 즉, 복사가 전혀 없고 교환 작업이 훨씬 더 빠릅니다. 이동 할당은 std::move() 메서드를 호출하여 호출할 수 있습니다.

기본적으로 메서드에서 개체를 반환하려면 호출자가 액세스할 수 있는 위치에 로컬 개체 및 관련 데이터의 복사본을 만드는 작업이 포함됩니다(로컬 개체는 호출자가 액세스할 수 없고 메서드가 완료되면 사라짐). 내장형이 반환되는 경우 이 작업은 매우 빠르지만 큰 개체가 반환되는 경우 시간이 오래 걸릴 수 있습니다. 이동 생성자를 사용하면 프로그래머가 이 기본 동작을 재정의하고 대신 호출자에게 반환되는 개체를 로컬 개체와 연결된 힙 데이터로 지정하여 로컬 개체와 연결된 힙 데이터를 "재사용"할 수 있습니다. 따라서 복사가 필요하지 않습니다.

로컬 개체(즉, 스택의 개체) 생성을 허용하지 않는 언어에서는 모든 개체가 힙에 할당되고 항상 참조에 의해 액세스되기 때문에 이러한 유형의 문제가 발생하지 않습니다.


Chris B

다음은 Bjarne Stroustrup의 "The C++ Programming Language" 책 의 답변입니다. 영상이 보기 싫으시다면 아래 글을 보시면 됩니다.

이 스니펫을 고려하십시오. operator+에서 반환하는 것은 결과를 지역 변수 res 에서 복사하고 호출자가 액세스할 수 있는 곳으로 복사하는 것을 포함합니다.

 Vector operator+(const Vector& a, const Vector& b) { if (a.size()!=b.size()) throw Vector_siz e_mismatch{}; Vector res(a.size()); for (int i=0; i!=a.size(); ++i) res[i]=a[i]+b[i]; return res; }

우리는 정말로 사본을 원하지 않았습니다. 우리는 함수에서 결과를 얻고 싶었을 뿐입니다. 따라서 Vector를 복사하는 대신 이동해야 합니다. 다음과 같이 이동 생성자를 정의할 수 있습니다.

 class Vector { // ... Vector(const Vector& a); // copy constructor Vector& operator=(const Vector& a); // copy assignment Vector(Vector&& a); // move constructor Vector& operator=(Vector&& a); // move assignment }; Vector::Vector(Vector&& a) :elem{a.elem}, // "grab the elements" from a sz{a.sz} { a.elem = nullptr; // now a has no elements a.sz = 0; }

&&는 "rvalue 참조"를 의미하며 rvalue를 바인딩할 수 있는 참조입니다. "rvalue"'는 "할당의 왼쪽에 나타날 수 있는 것"을 대략적으로 의미하는 "lvalue"를 보완하기 위한 것입니다. 따라서 rvalue는 함수 호출에 의해 반환된 정수 및 벡터에 대한 operator+() res 로컬 변수와 같이 대략 "할당할 수 없는 값"을 의미합니다.

이제 명령문은 return res; 복사하지 않습니다!


Rob Pei

출처 : http:www.stackoverflow.com/questions/3106110/what-is-move-semantics

반응형