{{ label!='' ? 'Label : ' : (q!='' ? '검색 : ' : '전체 게시글') }} {{ label }} {{ q }} {{ ('('+(pubs|date:'yyyy-MM')+')') }}

[C++] The C++ Programming Language: 3. A Tour of C++: Abstraction Mechanism (3)

 3.3.2 Moving Containers

  우리는 복사 생성자와 복사 대입 연산자를 정의함으로써 복사를 제어할 수 있습니다. 하지만 복사는 큰 컨테이너의 경우 큰 비용이 들 수 있습니다. 다음을 생각해보세요:

1
2
3
4
5
6
7
8
9
Vector operator+(const Vector& a, const Vector& b)
{
    if (a.size()!=b.size())
        throw Vector_size_mismatch{};
    Vector.res(a.size());
    for (int i=0; i!=a.size(); ++i)
        res[i]=a[i]+b[i];
    return res;
}
cs

  +로부터의 반환은 결과를 지역 변수 res에서 복사하고 어딘가에 놓아 호출자가 이를 접근할 수 있도록 하는 행위를 포함합니다. 아마 우리는 +를 다음처럼 사용하겠죠:

1
2
3
4
5
6
7
void f(const Vector& x, const Vector& y, const Vector& z)
{
    Vector r;
    // ...
    r = x+y+z;
    // ...
}
cs

  이 예제에서 Vector의 복사는 최소한 2번 이루어집니다.(+ 연산자 한 번에 한 번씩) 만약 Vector가 큰 경우, 가령 10,000 개의 double을 포함한다고 한다면 이는 곤란하죠. operator+()에 있는 res는 복사 이후에 다시 사용되지 않는다는 점이 가장 문제입니다. 우리는 딱히 복사를 원하지 않았죠. 그저 함수의 결과를 얻는 것이 필요했을 뿐입니다. 우리는 Vector를 복사하는 것보다는 이동하는 것을 원합니다. 다행히도, 그 의도를 담아 다음처럼 작성할 수 있습니다:

1
2
3
4
5
6
7
8
9
class Vector {
    // ...

    Vector(const Vector& a); // copy constr uctor
    Vector& operator=(const Vector& a); // copy assignment

    Vector(Vector&& a); // move constr uctor
    Vector& operator=(Vector&& a); // move assignment
};
cs

  이 정의는 컴파일러가 이동 생성자(move constructor)를 선택해 함수의 반환값을 이동시키도록 구현합니다. 

  보통, Vector의 이동 생성자는 다음과 같이 정의됩니다:

1
2
3
4
5
6
7
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;
}
cs

  &&의 의미는 "rvalue reference(우항 참조)"입니다. rvalue의 값을 참조할 수 있도록 해주죠. "rvalue"의 의미는 "lvalue"를 보완하기 위한 것이며, lvalue는 쉽게 말하면 "대입문에서 좌측에 나타날 수 있는 것"이죠. 따라서 rvalue는 함수 호출에 의해 반환된 정수와 같이 여러분이 할당할 수 없는 값입니다. rvalue reference는 누구도 할당할 수 없는 참조인 셈이죠. operator+()의 지역 변수 res는 하나의 예제가 될 수 있습니다.

  이동 생성자는 const 인자를 받지 않습니다: 이후에 이동 생성자는 인자들의 값을 삭제해야 합니다. 이동 대입 연산자도 마찬가지입니다.

  이동 연산은 rvalue reference가 initializer나 대입문의 우측으로 사용될 때 응용됩니다.

  이동 이후에, 본래 그 값을 갖고 있던 객체는 소멸자를 실행시킬 수 있는 상태여야만 합니다. 일반적으로, 해당 객체에게 대입 역시 할 수 있어야겠죠. (17.5, 17.6.2)

  프로그래머는 그 값이 더이상 사용되징 않을 것임을 알 수 있지만, 컴파일러는 우리가 생각한 만큼 영리하지 않기 때문에, 프로그래머는 다음과 같이 명시해주어야 합니다:

1
2
3
4
5
6
7
8
9
10
11
Vector f()
{
    Vector x(1000);
    Vector y(1000);
    Vector z(1000);
    // ...
    z = x; // we get a copy
    y = std::move(x); // we get a move
    // ...
    return z; // we get a move
};
cs

  표준 라이브러리 함수인 move()는 인자의 rvalue reference를 반환합니다.

  return 직전의 상황은 다음과 같습니다:


  z가 소멸되었을 때, (return에 의해)역시 이동되었을 것이며, 따라서 x와 같이 비어있을 것입니다.


3.3.3 Resource Management

  생성자, 복사 연산, 이동 연산, 소멸자를 정의함으로써 프로그래머는 자원의 생명주기의 완전한 제어를 제공할 수 있습니다. 더 나아가, 이동 생성자는 객체를 한 scope에서 다른 것으로 저렴하고 간단하게 이동시킬 수 있게 합니다. 우리가 scope 밖으로 복사되길 바라지 않는 객체를 손쉽게 옮길 수 있죠. 병렬적 작업을 표현하는 표준 라이브러리 thread와 수백만의 double 값이 있는 Vector를 생각해봅시다. 전자는 복사할 수 없고, 후자는 복사하고 싶지 않죠.

1
2
3
4
5
6
7
8
9
10
11
12
13
std::vector<thread> my_threads;
 
Vector init(int n)
{
    thread t {heartbeat}; // run heartbeat concurrently (on its own thread)
    my_threads.push_back(move(t)); // move t into my_threads
    // ... more initialization ...
    Vector vec(n);
    for (int i=0; i<vec.size(); ++i) vec[i] = 777;
    return vec; // move res out of init()
}
 
auto v = init(); // start heartbeat and initialize v
cs

이는 많은 경우에 포인터를 사용하는 것 대신 Vector, thread와 같이 리소스 핸들을 만듭니다. 실제로 표준 라이브러리 unique_ptr과 같은 "스마트 포인터"는 그들 스스로 자원을 제어하죠. 저는 thread를 유지하기 위해 표준 라이브러리 vector를 사용하였습니다. 아직 우리는 Vector를 엘리먼트 타입으로 매개변수화할 수 없기 때문이죠.

  아주 비슷한 방법으로 new와 delete는 응용 코드에서 사라집니다. 리소스 핸들로 포인터를 사라지게 만들 수 있죠. 두 경우 모두 결과는 간단해지고 코드를 유지보수하기 쉽게 만들며, 오버헤드가 없습니다. 특히, 강력한 자원 안전(strong resource safety)을 달성할 수 있습니다. 곧, 자원 누수를 막을 수 있다는 의미이죠. 예제들은 메모리를 유지하는 vector, 시스템 스레드를 유지하는 thread, 파일 핸들을 유지하는 fstream입니다.


3.3.4 Suppressing Operations

  계층을 지닌 클래스에게 기본 복사 또는 이동을 사용하는 것은 보통 참사를 불러일으킵니다: 간단히 이야기하면, 부모의 포인터만 주어지고 파생된 클래스가 어떤 멤버를 지녔는지는 모릅니다. 우리는 그들을 어떻게 복사해야할지 모르죠. 따라서 최선은 보통 기본 복사와 이동 연산을 삭제하는 것입니다. 곧, 그들 두 연산의 정의를 지워버리는 것이죠.

1
2
3
4
5
6
7
8
9
class Shape {
public:
    Shape(const Shape&=delete// no copy operations
    Shape& operator=(const Shape&=delete;
    Shape(Shape&&=delete// no move operations
    Shape& operator=(Shape&&=delete;
    ˜Shape();
    // ...
};
cs

  이제 Shape의 복사를 시도하는 것은 컴파일러에 의해 제지될 것입니다. 만약 클래스 계층에서 객체를 복사할 일이 생겼다면, 특정한 복사 함수를 작성해야 하죠.

  이러한 특수 경우에서, 여러분들이 복사 혹은 이동 연산을 삭제하는 것을 잊어버렸더라도 괜찮습니다. 이동 연산은 소멸자를 명시적으로 선언한 클래스에서는 암시적으로 생성되지 않습니다. 더 나아가, 이런 경우에서 복사 연산자의 생성은 권장되지 않습니다. 이는 명시적으로 소멸자를 정의해야 하는 좋은 이유가 될 것입니다. 컴파일러가 복사 연산자를 암시적으로 제공하지 않게 될 것이니까요.

  부모 클래스는 우리가 복사를 원하지 않는 객체들 중 하나의 예에 불과합니다. 리소스 핸들은 단순히 그들의 멤버를 복사한다고 해서 일반적으로 복사할 수 있는 것은 아닙니다. =delete 메커니즘을 사용하는 것은 일반적이죠. 이를 통해 어떤 연산이든 억제하는 데에 사용하도록 하십시오.

댓글

이 블로그의 인기 게시물

[코딩의탑] 5층: 포탈(Portal), 더 나아가기

[코딩의탑] 4층: 툰 쉐이딩

[코딩의탑] 3층: 바다 렌더링