leesangwon0114

I am Research Engineer. Currently working in KT.

C++Intermediate 22. move semantics#1

18 Nov 2018 » c++

codenuri 강석민 강사 강의 내용기반으로 정리한 내용입니다.


copy constructor

class Point
{
    int x, y;
public:
    Point(int a = 0, int b = 0) : x(a), y(b) {}

    // Point(Point p) // Point p = p1. 결국 복사 생성자가 무한히 호출
    // Point(Point& p) // 빌드하는데의 문제는 없음. p3를 만들 때 임시객체이므로 lvalue 로 참조 불가능함(lvalue 객체만 인자로 받을 수 있다. 함수 리턴 값으로 반환되는 임시객체를 받을 수 없다.)
    Point(const Point& p) // lvalue 객체와 rvalue 객체를 모두 받을 수 있음
    {
        // 모든 멤버 복사
    }
};

Point foo()
{
    Point ret(0, 0);
    return ret;
}

int main()
{
    Point p1(1,1); // 생성자 호출
    Point p2(p1); // Point(Point) : 복사 생성자

    Point p3(foo());
}

move semantics

class Cat
{
    char& name;
    int age;
public:
    Cat(const char* n, int a) : age(a)
    {
        name = new char[strlen(n) + 1];
        strcpy(name, n);
    }
    ~Cat() { delete[] name; }
    // 깊은 복사로 구현한 복사 생성자
    Cat(const Cat& c) : age(c.age)
    {
        name new char[strlen(c.name) + 1];
        strcpy(name, c.name);
    }

};

int main()
{
    Cat c1("NABI", 2);
    Cat c2 = c1; // runtime error (메모리 두번 참조로 하나 파괴시 다음번 또 파괴하려함 -> 복사생성자 깊은 복사 이용)
}

class Cat
{
    char& name;
    int age;
public:
    Cat(const char* n, int a) : age(a)
    {
        name = new char[strlen(n) + 1];
        strcpy(name, n);
    }
    ~Cat() { delete[] name; }
    // 깊은 복사로 구현한 복사 생성자
    Cat(const Cat& c) : age(c.age)
    {
        name new char[strlen(c.name) + 1];
        strcpy(name, c.name);
    }
    // 소유권 이전(자원전달)의 복사 생성자-> rvalue reference 로 임시객체만 올 수 있도록함
    Cat(Cat&& c) : age(c.age), name(c.name) // 이동생성자(move)라 불림(C++11 에서는 두 개 만듬)
    {
        c.name = 0; // 자원 포기
    }
};

Cat foo() // 값 리턴 : 임시객체(rvalue)
{
    Cat cat("NABI", 2);
    return cat;
}

int main()
{
    Cat c = foo(); // 임시객체
}

임시객체로 만들고 복사 후 파괴하면… 자원이 비효율적임

name 을 깊은 복사하지말고 주소만 가르키게 함

임시객체가 파괴될 때 주소 복사 후 임시객체의 name 주소를 0으로 바꾸어 임시객체 파괴 시 0은 delete 되는 데 문제 없음

따라서 성능 향상됨


move 정리

class Test
{
    int* resource;
public:
    Test() {} // 자원 할당
    ~Test() {} // 자원 해지

    // 복사생성자 : 깊은 복사 또는 참조계수
    // 인자로 lvalue 와 rvalue를 모두 받을 수 있다
    Test(const Test& t) { cout << "Copy" << endl; }

    // Move 생성자 : 소유권 이전(자원 전달)
    // rvalue 만 전달 받을 수 있다.
    Test(Test&& t) { cout << "Move" << endl; }
};

int main()
{
    Test t1;
    Test t2 = t1; // 복사생성자
    Test t3 = Test(); // move 생성자(단, move 를 안만들면 복사생성자)
}

move 생성자를 안만들어도 에러가 없지만 만들면 성능향상을 볼 수 있음

Test t3 = Test(); 일 경우 컴파일러 최적화로 컴파일 시 바로 생성자 불러서 만들어 버려 Copy 한번 밖에 안불리는 것 볼 수 있음(g++ 에서 -fno-elide-constructors 옵션을 주면됨)


std::move

class Test
{
public:
    Test() {}
    ~Test() {}
    Test(const Test& t) { cout << "Copy" << endl; }
    Test(Test&& t) { cout << "Move" << endl; }
};

int main()
{
    Test t1;
    Test t2 = t1;     // Copy
    Test t3 = Test(); // Move -> g++ -fno-elide-constructors 옵션 붙여야함
    Test t4 = static_cast<Test&&>(t1); // Move - 복사가 아닌 move 생성자를 호출해 달라.

    Test t5 = move(t2); // move 가 내부적으로 위처럼 캐스팅한다.
}

Copy Move Move 순으로 출력됨


class Test
{
public:
    Test() {}
    ~Test() {}
    Test(const Test& t) { cout << "Copy" << endl; }
    Test(Test&& t) { cout << "Move" << endl; }

    Test& operator=(const Test& t) { return *this; } // 복사 대입연산자
    Test& operator=(Test&& t) { return *this; } // move 대입연산자
};

int main()
{
    Test t1;
    Test t2 = t1;     // 초기화. 복사 생성자
    t2 = t1;          // 대입. 대입 연산자

    t2 = move(t1);
}

복사 대입과 move 대입 연산자도 만들어야한다.