공부/C || C++

C++ 11 default, delete 키워드

sudo 2021. 7. 15. 19:13

C++ 11에서는 자체적으로 선언하지 않을 경우 기본 생성자, 복사 생성자, 복사 할당 연산자 및 소멸자를 자동으로 생성하고 이러한 함수는 특수 멤버 함수 라고 한다. 이런 특수 멤버 함수에는 몇가지 규칙이 있다(아래 규칙들은 microsoft docs에 있는걸 가져왔다).

  • 생성자가 명시적으로 선언된 경우 기본 생성자가 자동으로 생성되지 않습니다.
  • 가상 소멸자가 명시적으로 선언된 경우 기본 소멸자가 자동으로 생성되지 않습니다.
  • 이동 생성자 혹은 이동 할당 연산자가 명시적으로 선언된 경우 다음과 같습니다.
    • 복사 생성자가 자동으로 생성되지 않습니다.
    • 복사 할당 연산자가 자동으로 생성되지 않습니다.
  • 복사 생성자, 복사 할당 연산자, 이동 생성자, 이동 할당 연산자 또는 소멸자가 명시적으로 선언된 경우 다음과 같습니다.
    • 이동 생성자가 자동으로 생성되지 않습니다.
    • 이동 할당 연산자가 자동으로 생성되지 않습니다.
  • 또한 C++ 11 표준은 다음 추가 규칙을 지정합니다.
    • 복사 생성자나 소멸자가 명시적으로 선언된 경우 복사 할당 연산자가 자동으로 생성되지 않습니다.
    • 복사 할당 연산자나 소멸자가 명시적으로 선언된 경우 복사 생성자가 자동으로 생성되지 않습니다.

 

나머지는 이미 알고 있지만 3번 규칙은 정말 그런지 이전에 Rvalue 포스팅에서 썼던 코드를 가져와보자


// MemoryBlock.h
#pragma once
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

class MemoryBlock
{
public:

    // 버퍼 크기를 넘겨받는 생성자
    explicit MemoryBlock(size_t length)
        : _length(length)
        , _data(new int[length])
    {
        std::cout << "In MemoryBlock(size_t). length = "
            << _length << "." << std::endl;
    }

    // 소멸자
    ~MemoryBlock()
    {
        std::cout << "In ~MemoryBlock(). length = "
            << _length << ".";

        if (_data != NULL)
        {
            std::cout << " Deleting resource.";
            // 리소스 삭제
            delete[] _data;
        }

        std::cout << std::endl;
    }

    
    // 복사 생성자
    MemoryBlock(const MemoryBlock& other)
        : _length(other._length)
        , _data(new int[other._length])
    {
        std::cout << "In MemoryBlock(const MemoryBlock&). length = "
            << other._length << ". Copying resource." << std::endl;

        std::copy(other._data, other._data + _length, _data);
    }

    // 대입 연산자
    MemoryBlock& operator=(const MemoryBlock& other)
    {
        std::cout << "In operator=(const MemoryBlock&). length = "
            << other._length << ". Copying resource." << std::endl;

        if (this != &other)
        {
            // 기존 리소스 삭제
            delete[] _data;

            _length = other._length;
            _data = new int[_length];
            std::copy(other._data, other._data + _length, _data);
        }
        return *this;
    }
    

    // Move 생성자
    MemoryBlock(MemoryBlock&& other)
        : _data(NULL)
        , _length(0)
    {
        std::cout << "In MemoryBlock(MemoryBlock&&). length = "
            << other._length << ". Moving resource." << std::endl;

        _data = other._data;
        _length = other._length;

        // 소멸자에 data가 NULL이 아니면 delete해주는데, Move 생성자이므로
        // 생성자에서 할당된 메모리 영역을 가리키고 있는 _data를 나 자신이 _data를 복사해서 갖고 있으므로,
        // 이 포인터가 가리키는 영역은 메모리가 해제되지 않도록 아래와 같이 NULL을 할당 해줘야한다
        other._data = NULL;
        other._length = 0;
    }

    // Move 대입 연산자
    MemoryBlock& operator=(MemoryBlock&& other)
    {
        std::cout << "In Move operator=(MemoryBlock&&). length = "
            << other._length << "." << std::endl;

        if (this != &other)
        {
            // 기존 리소스 삭제
            delete[] _data;

            // Move 대입 연산자는 copy로 포인터가 가리키는 내용을 할 필요 없이
            // 포인터만 복사해도 됨
            _data = other._data;
            _length = other._length;

            // 소멸자에 data가 NULL이 아니면 delete해주는데, Move 생성자이므로
            // 생성자에서 할당된 메모리 영역을 가리키고 있는 _data를 나 자신이 _data를 복사해서 갖고 있으므로,
            // 이 포인터가 가리키는 영역은 메모리가 해제되지 않도록 아래와 같이 NULL을 할당 해줘야한다
            other._data = NULL;
            other._length = 0;
        }
        return *this;
    }
    

    size_t Length() const
    {
        return _length;
    }

private:
    size_t _length;  // 리소스 길이
    int* _data;      // 리소스
};


int main()
{
    // MemoryBlock에 대한 vector를 생성하여 두 개의 원소를 추가
    vector<MemoryBlock> v;

    cout << endl;

    MemoryBlock mem1(25);

    v.push_back(mem1); // copy constructor 호출

    cout << endl;

    MemoryBlock mem2(75);

    v.push_back(mem2); // copy constructor 호출

    cout << endl;

    MemoryBlock mem3(50);

    v[0] = mem3; // 대입 연산자 호출

    cout << endl;
}

 

당연히 main에서 push_back인자로는 lvalue를 넘겨주고 있으므로 move 생성자나 move 대입 생성자 아니라 v.push_back(mem1), v.push_back(mem2)에서는 copy constructor가, v[0] = mem3에서는 대입 연산자가 불린다. 그런데 여기서 move 생성자와 move 대입 생성자만 남기고, copy constructor와 대입 연산자를 지우면 어떻게 될까? 위의 규칙대로라면 move 생성자와 move 대입 생성자를 명시적으로 선언했으니 복사 생성자와 대입 연산자가 자동으로 생성되지 않을것이고 main에서는 복사 생성자와 대입 연산자를 필요로 하니 컴파일 오류가 떠야 할 것이다. 결과를 보면 

오 정말 오류가 뜬다... 그런데 삭제된 함수라고 하는데 이게 뭘까. 그게 이번 포스팅중에서 delete와 관련있다.

 

delete 키워드는 컴파일러가 자동으로 만들어주는 것들(위에서 언급한 디폴트 생성자나 디폴트 복사 생성자같은 것들)을 막는 기능이다. 이런걸 언제 사용하느냐면 예를 들어서 어떤 객체가 (나도 모르는 사이)복사되는 것을 방지하고 싶다면 어떻게 하면 될까. 그때 이 delete 키워드를 사용하면 된다. 예를 들어 아래와 같은 코드가 있다고 하자.

#include <iostream>

class NonCopy
{
private:
	int Num;
public:
	NonCopy(int n) { Num = n; }
	~NonCopy() {}

	NonCopy(const NonCopy& n) = delete;
};

int main()
{
	NonCopy noncopy_(3);

	NonCopy noncopy2_ = noncopy_; // 복사 생성자 호출 시도 -> error!
}

delete가 적힌 copy constructor를 호출하려고 하므로 다음과 같은 컴파일 오류가 뜬다.

 

 

그렇다면 default 키워드는 무슨 의미일까? default 키워드는 코드 작성자가 "이 클래스는 얕은 복사를 해도 되므로, 컴파일러가 자동으로 만들어주는 디폴트 복사 생성자와 디폴트 대입 연산자를 써도 된다" 라는 것을 명시적으로 알려주기 위해 사용된다. 멤버 변수가 동적할당이 된 것이 아니라 그냥 단순한 값만으로 이루어져 있다면 얕은 복사를 해도 문제가 생기진 않기 때문에 이럴때 사용한다. 혹은 클래스 내에서 메모리가 할당된 곳을 가리키는 포인터를 멤버 변수로 갖고 있는데 굳이 이 클래스의 소멸자 내에서 메모리 해제를 해주지 않아도 될 때 명시적으로 알려주기 위해서 쓰이는 것도 보았다. 예를 들어서 이전에 메모리 풀 공부할 때 참고한 깃헙 코드를 보면 

https://github.com/SnowFleur/2020-Pool-Patterns

소멸자에 default 키워드가 붙어져 있는데 freeBlock*를 여기 클래스에서 해제하지 않는다는 것을 명시적으로 알려주기 위해서 쓰였다.

 

Reference

https://docs.microsoft.com/ko-kr/cpp/cpp/explicitly-defaulted-and-deleted-functions?view=msvc-160 

 

명시적으로 기본 설정 및 삭제된 함수

자세히 알아보기: 명시적으로 기본 설정 및 삭제 된 함수

docs.microsoft.com

https://woo-dev.tistory.com/100

 

[C++] 가독성을 위해 default와 delete 키워드를 사용하기 (클래스)

가독성을 위해 default와 delete 키워드를 사용하자 [배경] 클래스 작성 시 우리가 직접 작성하지 않아도 기본적으로 컴파일러가 생성해주는 것들이 있다. 그 대표적인 예로 기본 생성자, 기본 소멸

woo-dev.tistory.com