빌드 시간을 줄이기 위해 클래스에 pimpl 관용구를 적용시키는 경우 전방 선언한 클래스나 구조체의 포인터를 사용하게 된다.

그리고 이를 동적할당하여 사용 후 소멸자에서 해제를 시키는 것이 보통일 것이다.

 

// Widget.h
class Widget {
public:
    Widget();
    ~Widget();
private:
    struct Impl; // 전방 선언
    Impl* pImpl;
};

// Widget.cpp
struct Widget::Impl {
    std::string name;
    std::vector<double> data;
    Gadget g1, g2, g3;
};

Widget::Widget() : pImpl(new Impl) {}
Widget::~Widget() { delete pImpl; }

하지만 이제는 원시 포인터와 new, delete를 직접 사용하는 대신 스마트 포인터를 사용하는 것이 좋다는 것을 안다.

 

// Widget.h
class Widget {
public:
    ... // 소멸자에서 delete를 해줄 필요가 없다
private:
    struct Impl; // 전방 선언
    std::unique_ptr<Impl> pImpl;
};

// Widget.cpp
...
Widget::Widget() : pImpl(std::make_unique<Impl>()) {}

스마트 포인터이기 때문에 소멸자에서 delete를 해줄 필요가 없다.

위의 코드들 자체는 컴파일이 잘 되지만 클라이언트에서 Widget 객체를 선언하여 사용하려고 할 때에는 컴파일 에러가 발생하게 된다.

 

#include "Widget.h"

Widget w;

w가 파괴되는 시점에 소멸자가 호출되는데, 스마트 포인터로 변경하면서 소멸자를 작성하지 않았기 때문에 컴파일러가 소멸자를 자동으로 작성하게 된다. 이 때 pImpl의 소멸자인 unique_ptr의 기본 삭제자를 호출한다.

unique_ptr의 기본 삭제자는 소유중인 원시 포인터에 delete를 적용하기 전에 혹시라도 원시 포인터가 불완전한 타입을 가리키고 있지는 않은지를 static_assert를 이용해서 점검하게 된다. 그런데 여기서 불완전한 타입을 가리키고 있다고 판단하게 되어 에러가 발생하는 것이다.

 

컴파일러가 자동으로 작성하는 특수 멤버 함수는 암묵적으로 inline이고 Widget.h 안에는 Impl에 대한 정의가 되어있지 않아서 자동으로 작성된 소멸자가 호출되는 순간에는 불완전한 타입이 되는 것이다.

 

이 문제는 unique_ptr<Widget::Impl>을 파괴하는 코드가 만들어지는 지점에서 완전한 타입이 되면 바로 해결된다.

Widget::Impl의 정의는 cpp에 되어있기 때문에 소멸자의 호출이 헤더가 아닌 cpp에서 이뤄지면 될 것이다.

 

그럼 이제 어떻게 하면 되는가.

소멸자의 선언과 정의를 분리시켜서 명시적으로 작성해주면 된다.

 

// Widget.h
class Widget {
public:
    Widget();
    ~Widget();
    ...
private:
    ...
};

// Widget.cpp
Widget::~Widget() {} // Widget::~Widget() = default;

불완전한 타입에 대한 문제는 해결되었다.

 

Pimpl 관용구를 사용하는 클래스는 이동 연산들을 지원하기에 아주 좋은 클래스이다.

심지어 별도로 작성하지 않아도 컴파일러가 자동으로 작성해주는 이동 연산들이 요구에 딱 맞는 이동을 수행한다.

하지만 불완전한 타입에 대한 문제를 해결하느라 소멸자를 명시적으로 만들어주는 바람에 이동 연산은 자동으로 생성되지 않는다. 하지만 이것 역시 명시적으로 작성해주면 된다.

 

// Widget.h
class Widget {
public:
    Widget(Widget&& rhs); // 여기에 default를 적으면 안됨
    Widget& operator=(Widget&& rhs);
    ...
};

// Widget.cpp
Widget::Widget(Widget&& rhs) = default;
Widget& operator=(Widget&& rhs) = default;

이동연산에 대한 문제도 해결하였다.

 

하지만 만약 Gadget이 string, vector와 같이 복사가 가능한 타입이라면 복사 연산을 지원해주는것이 합당하다.

그런데 복사 연산은 이동 연산과 달리 직접 구현해주어야 한다. 왜냐면 unique_ptr같은 이동 전용 타입이 있는 클래스에서는 컴파일러가 복사 연산들을 작성해주지 않을뿐더러 설령 작성된다 하더라도 얕은 복사를 수행하기 때문이다.

 

// Widget.h
class Widget {
public:
    Widget(const Widget& rhs);
    Widget& operator=(const Widget& rhs);
    ...
};

// Widget.cpp
Widget::Widget(const Widget& rhs) : pImpl(nulltpr) {
    if (rhs.pImpl) pImpl = std::make_unqiue<Impl>(*rhs.pImpl);
}

Widget& Widget::operator=(const Widget& rhs) {
    if (!rhs.pImpl) pImpl.reset();
    else if (!pImpl) pImpl = std::make_unique<Impl>(*rhs.pImpl);
    else *pImpl = *rhs.pImpl;
    
    return *this;
}

복사 연산을 직접 구현했을뿐 기존처럼 선언과 정의를 분리시켰다.

 

 

재미있게도 unique_ptr와 shared_ptr 둘 중 어느것을 사용하느냐에 따라 여지껏 언급한 문제해결 방법이 유효할수도, 필요없을수도 있다는 것이다.

shared_ptr의 삭제자는 타입의 일부가 아니라 제어 블록에서 같은 shared_ptr과 공유되기 때문에 런타임에 크기가 더 커지고 속도도 약간 느려지지만 삭제자가 호출될 때 원시 포인터의 타입이 완전한 타입이어야 한다는 제약 조건이 사라진다.

 

Pimpl 관용구를 사용함에 있어서 unique_ptr이나 shared_ptr사이에서 어떤 절충 관계가 존재하는 것은 아니다.

Widget과 Widget::Impl 사이의 관계는 독점적 소유 관계이기 때문에 unique_ptr이 더 잘 맞아떨어질 뿐이다.

상황에 따라 shared_ptr를 선택하는 경우에는 불완전한 타입에 대한 문제를 신경쓰지 않아도 된다는 점만 알면 된다.

 

◾ Pimpl 관용구는 클래스 구현과 클래스 클라이언트 사이의 컴파일 의존성을 줄임으로써 빌드 시간을 감소한다.

◾ std::unique_ptr 타입의 pImpl 포인터를 사용할 때에는 특수 멤버 함수들은 클래스 헤더에 선언하고 구현 파일에서 구현해야 한다. 컴파일러가 기본으로 작성하는 함수 구현들이 사용하기에 적합한 경우에도 그렇게 해야 한다.

◾ 위의 조언은 std::unique_ptr에 적용될 뿐, std::shared_ptr에는 적용되지 않는다.

make_shared는 C++11의 표준이지만 make_unique는 C++14의 표준이다.

allocate_shared라는 함수도 있는데 이건 커스텀 할당자를 포함한 shared_ptr를 만들어준다.

 

각설하고 왜 make 함수를 선호하라고 하는건지 알아보도록 하자.

 

auto upw1(std::make_unique<Widget>());
std::unique_ptr<Widget> upw2(new Widget);

auto spw1(std::make_shared<Widget>());
std::shared_ptr<Widget> spw2(new Widget);

둘 다 스마트 포인터를 생성하는 문법이다. 하지만 make 함수로 생성하는 경우 auto를 사용하기 때문에 타입을 중복해서 작성하지 않아도 된다.

 

또한 예외 안전성과 관련이 있다.

 

void processWidget(std::shared_ptr<Widget> spw, int priority);
int computePriority(); // 우선순위를 반환하는 함수
...

processWidget(std::shared_ptr<Widget>(new Widget), computePriority());

위와 같은 코드에서 새로운 Widget 객체가 동적 할당되고 shared_ptr의 생성자가 호출된 후 computePriority 함수가 호출되기를 기대한다. 하지만 반드시 그런것은 아니다. new Widget은 shared_ptr의 생성자보다 먼저 평가되는 것은 확실하지만 computePriority는 그보다 먼저 호출될수도, 아닐수도 있는 것이다.

심지어 중간에 호출될수도 있다. 그런 경우에 computePriority가 예외를 던졌다면 new Widget에 의해 객체가 동적할당 되었지만 shared_ptr의 생성자를 호출하지 못해서 자원이 새는 경우가 발생하게 되는 것이다.

 

processWidget(std::make_shared<Widget>(), computePriority());

하지만 make_shared로 shared_ptr를 생성한다면 둘의 호출 순서가 바뀔수는 있어도 중간에 호출되는 일은 발생하지 않기 때문에 근본적으로 자원 누수가 발생할 가능성이 끼어들지 못한다.

 

 

std::shared_ptr<Widget> spw(new Widget);

설령 아무런 예외가 발생하지 않는다고 해도 또다른 차이가 있다.

이 코드는 메모리 할당이 한번만 이루어질것 같지만 실제로는 두번 이루어진다. 동적 할당된 Widget 객체 말고도 제어 블록을 위한 또 다른 메모리 할당이 일어난다.

대신 make_shared를 사용하면 Widget 객체와 제어 블록 모두를 담을 수 있는 크기의 메모리 공간을 한번에 할당하기 때문에 프로그램의 정적 크기가 줄어들뿐만 아니라 메모리를 한번만 할당하므로 실행 속도 역시 빨라진다.

 

 

여기까지 언급했을때는 make 함수 사용이 모든 상황에서 좋아보이지만 아쉽게도 그렇지는 않다. 항상 사용하라는 것이 아니고 선호하라는 것이다.

그에 대한 몇가지 이유들이 존재한다.

 

첫째로 예전에도 언급했듯이 make함수는 커스텀 삭제자를 지정할 수 없다.

두번째로 중괄호 초기화를 사용하는 타입의 객체의 초기값을 완벽하게 전달할 수 없다. make 함수들은 내부적으로 매개변수들을 객체의 생성자를 완벽하게 전달하는데에 있어서 중괄호가 아닌 괄호를 사용한다. 만약 중괄호 초기화를 사용하는 객체의 초기값을 완벽하게 전달하고 싶다면 new를 사용하거나 초기화 리스트 객체를 미리 만들어서 make 함수에 전달해야한다.

 

여기까지가 unique_ptr의 make 함수 사용시의 문제점이고 안타깝게도 shared_ptr는 두가지가 더 있다.

 

만약 어떤 클래스가 기본으로 제공되는 new, delete가 적합하지 않아서 자신만의 메모리 관리를 위해 두 연산자를 오버로딩했다고 하자. 보통 이런 경우는 정확히 해당 객체 크기만큼의 메모리 조각들의 할당과 해제를 처리하는데 특화되어있다. 이런 것들은 shared_ptr의 커스텀 할당자와 커스텀 삭제자와 궁합이 잘 맞지 않는다.

왜냐면 커스텀 할당자가 요구하는 메모리 조각의 크기는 해당 객체의 크기가 아닌 제어 블록의 크기까지 더한것이기 때문이다. 그래서 클래스 고유의 new, delete 연산자가 있는 타입의 객체를 make 함수로 생성하는 것은 대체로 좋은 선택이 아니다.

 

 

weak_ptr이 객체의 만료 여부를 판단할 수 있는것은 제어 블록의 참조 횟수를 점검하기 때문이다. 그런데 참조 횟수가 0이 되었다고 객체와 더불어 제어 블록까지 소멸시켜 버리면 weak_ptr이 참조할 제어 블록이 사라지는 것이기 때문에 참조 횟수와 더불어 약참조 횟수까지 0이 되어야 제어 블록이 소멸된다.

make_shared를 통해 shared_ptr를 만든다면 메모리 할당을 한번만 한다고 했다. 이 뜻은 연속된 메모리 공간에 객체와 제어 블록을 배치한다는 것이고, 해제도 동시에 이루어져야 함을 의미한다. 하지만 weak_ptr가 존재하는 한, 참조 횟수가 0이 되어 객체를 사용할 수 없을지라도 메모리 공간은 계속 점유되기 때문에 마지막 shared_ptr의 파괴와 마지막 weak_ptr의 파괴 사이의 시간이 꽤 길다면 실제 객체가 파괴된 시점과 점유하던 메모리가 해제되는 시점 사이에 시간 지연이 생길 수 있다.

 

하지만 이런 경우에 make 함수 대신 new를 통해 메모리를 두번 할당하여 생성시킨다면 별개의 공간에 존재하기 때문에 마지막 shared_ptr가 소멸될 때, 객체 역시 소멸된다.

물론 예외 안전성의 문제를 무시할 수 없으므로 new를 통해 shared_ptr를 생성하는 경우에는 해당 문장에서는 오직 new의 결과를 shared_ptr의 생성자에 넘겨주는 작업만 해서 예외가 방출될 일이 존재하지 않도록 한다.

 

std::shared_ptr<Widget> spw(new Widget, cusDel); // 커스텀 삭제자
processWidget(spw, computePriority());

예외에 안전한 코드가 되었다. 하지만 한가지 아쉬운점이 있다면 spw가 왼값으로 넘어가기 때문에 복사 연산이 발생하게 된다. 실제로는 오른값으로 넘겨주던것을 예외에 안전한 코드를 작성하기 위해 왼값으로 만든것이기 때문에 이동 연산을 통해서 성능까지 챙겨주면 더 좋다.

 

std::shared_ptr<Widget> spw(new Widget, cusDel); // 커스텀 삭제자
processWidget(std::move(spw), computePriority());

그래도 make 함수를 사용하지 않고 shared_ptr를 생성시킬 일이 그렇게 많지는 않을 것이다.

 

◾ new의 직접 사용에 비해, make 함수를 사용하면 소스 코드 중복의 여지가 없어지고, 예외 안전성이 향상되고, std::make_shared와 std::allocate_shared의 경우 더 작고 빠른 코드가 산출된다.

◾ make 함수의 사용이 불가능 또는 부적합한 경우로는 커스텀 삭제자를 지정해야 하는 경우와 중괄호 초기값을 전달해야 하는 경우가 있다.

◾ std::shared_ptr에 대해서는 make 함수가 부적합한 경우가 더 있는데, 두 가지 예를 들자면 (1) 커스텀 메모리 관리 기능을 가진 클래스를 다루는 경우와 (2) 메모리가 넉넉하지 않은 시스템에서 큰 객체를 자주 다루어야 하고 std::weak_ptr들이 해당 std::shared_ptr들보다 더 오래 살아남는 경우이다.

weak_ptr은 원시 포인터와 shared_ptr사이에 위치한 스마트 포인터이다. 객체를 안전하게 참조할 수 있지만 참조 횟수를 늘리지 않는다. 정확히는 약참조 횟수를 늘리는데 이는 객체의 수명과 관련이 없다.

weak_ptr은 shared_ptr의 단점인 순환참조나 이미 소멸된 자원을 가리키는 문제를 보완하는데 사용된다.

참조횟수를 증가시키지 않기 때문에 순환참조가 발생하지 않고, 역참조가 불가능하기 때문에 반드시 shared_ptr로 변환하여 사용해야 하는데 자원이 이미 소멸된 경우에는 비어있는 shared_ptr을 반환해주므로 문제가 발생하지 않는다.

 

std::shared_ptr<Widget> sp(new Widget);

std::weak_ptr<Widget> wp(sp);
std::weak_ptr<Widget> wp2(wp);

shared_ptr의 문제점을 보완하기 위한 스마트 포인터이기 때문에 원시 포인터를 통해 직접적인 생성은 불가능하고 shared_ptr이나 weak_ptr을 통해서만 생성이 가능하다.

 

weak_ptr로부터 shared_ptr을 생성하는 방법은 두 가지가 있다. 이 때, weak_ptr의 만료여부에 따라 동작이 다르다.

 

std::shared_ptr<Widget> spw(new Widget);

std::weak_ptr<Widget> wpw(spw);
/* 이후 swp 소멸 */

/* Case 1 */
std::shared_ptr<Widget> spw1 = wpw.lock(); // 가리키는 원시 포인터는 nullptr

/* Case 2 */
std::shared_ptr<Widget> spw2(wpw); // std::bad_weak_ptr 예외 발생

 

weap_ptr의 사용이 유용한 예를 몇가지 들어보도록 하자.

첫째로 어떤 함수의 호출 비용이 꽤 크고 그 반환값이 반복되어 사용되는 경우가 많다고 하면 그 반환값들을 캐싱하여 사용하는 것이 효율적일 것이다. 하지만 계속 캐시에 담아 둔다면 그 자체로 성능문제가 또 발생하기 때문에 더 이상 쓰이지 않는 값들은 캐시에서 삭제하는 것이 또 다른 최적화 방안이 될 것이다.

 

이 때, 팩토리 함수의 반환 타입을 unique_ptr(이전에는 좋다고 했지만)로 두는 것은 그다지 좋지 못하다. 왜냐면 캐시 값들이 수시로 삭제되기 때문에 weak_ptr를 사용하는것이 좋고 weak_ptr를 사용하려면 반드시 반환 타입이 shared_ptr이어야 한다.

 

두 번째 사례로 관찰자 패턴에서 유용하게 사용된다.

관찰 대상은 관찰자의 수명에는 관심이 없고 파괴된 관찰자에 접근하는지가 중요하다. 이 때 weak_ptr를 사용하면 만료 여부를 먼저 확인하고 관찰자에게 메시지를 날릴 수 있다.

 

 

weak_ptr는 shared_ptr과 크기가 같고 shared_ptr가 사용하는 제어 블록과 같은 제어 블록에 접근하여 사용한다.

 

◾ std::shared_ptr처럼 작동하되 대상을 잃을 수도 있는 포인터가 필요하면 std::weak_ptr를 사용하라.

◾ std::weak_ptr의 잠재적인 용도로는 캐싱, 관찰자 목록, 그리고 std::shared_ptr 순환 참조 방지가 있다.

unique_ptr은 이름에서 알수있듯이 독점적 소유권을 가지고 있다. unique_ptr은 복사가 허용되지 않고 이동만 가능하다.

소멸 시 자신이 가리키는 자원(원시 포인터)을 파괴한다. 기본적으로 소유하고 있는 원시 포인터에 delete를 적용해서 파괴하게 된다.

 

template<typename... Ts>
std::unique_ptr<Investment> makeInvestment(Ts&&... params); // C++11

unique_ptr은 흔히 팩토리 함수의 반환 타입으로 자주 쓰인다.

 

unique_ptr이 소멸하며 소유중인 원시 포인터를 파괴할 때 기본적으로 delete를 사용하지만 필요하다면 unique_ptr 객체를 생성할 때 커스텀 삭제자를 사용하도록 지정하는 것도 가능하다.

 

auto delInvmt = [](Investment* pInvestment) {
    makeLogEntry(pInvestment); // 로그 기록
    delete pInvestment;
};

template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)> makeInvestment(Ts&&... params) {
    std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt);
    ...
}

삭제하기 전에 로그를 남긴 뒤에 삭제하는 커스텀 삭제자를 만들수도 있다. 다만 타입이 파생클래스인 경우 기반클래스의 포인터를 통해 소멸자를 호출하기 때문에 반드시 가상 소멸자이어야 한다.

 

기본 삭제자를 사용하는 경우 unique_ptr의 객체 크기는 원시 포인터의 크기와 같을거라고 가정하는 것이 합당하지만 커스텀 삭제자를 사용하는 경우 크기가 증가하게 된다.

일반적으로는 함수 포인터를 삭제자로 지정한 경우 1워드만 증가하지만 상태를 많이 가지는 함수 객체라면 그 크기만큼 증가하게 된다. 그래서 가능하면 람다식으로 구현하는 것을 선호하는 것이 좋다.

 

unique_ptr에는 원시 포인터를 직접 대입하게 되면 컴파일 에러가 발생한다. 만약 이게 허용된다면 암묵적 변환이 성립하기 때문에 그렇다.

 

unique_ptr은 팩토리 함수 뿐만 아니라 pimpl 관용구의 구현에 더 잘 쓰인다. 이 내용은 추후 다루도록 한다.

 

std::shared_ptr<Investment> sp = makeInvestment(...);

또한 unique_ptr은 shared_ptr로의 변환도 매우 손쉽고 효율적이다.

팩토리 함수는 반환된 객체가 독점적인 소유인지, 소유권이 공유되는지를 미리 알 수 없다. 하지만 unique_ptr로 반환한다면 둘 중 어느것도 될 수 있는 유연성이 생긴다.

 

◾ std::unique_ptr는 독점 소유권 의미론을 가진 자원의 관리를 위한, 작고 빠른 이동 전용 스마트 포인터이다.

◾ 기본적으로 자원 파괴는 delete를 통해 일어나나, 커스텀 삭제자를 지정할 수도 있다. 상태 있는 삭제자나 함수 포인터를 사용하면 std::unique_ptr 객체의 크기가 커진다.

◾ std::unique_ptr을 std::shared_ptr로 손쉽게 변환할 수 있다.

특수 멤버 함수는 사용자가 작성하지 않아도 컴파일러가 자동으로 작성하는 기본 생성자, 소멸자, 복사 생성자, 복사 대입 연산자를 가리킨다.

이 함수들은 명시적으로 작성되어 있지 않은 상태에서 해당 함수를 사용하는 코드가 존재할 때만 작성된다.

컴파일러에 의해 작성된 특수 멤버 함수는 기본적으로 public, inline이고 가상 소멸자가 존재하는 기반 클래스를 상속받아서 파생 클래스 역시 가상 소멸자가 생성되는 경우를 제외하고는 모두 비가상 함수이다.

 

class Widget {
public:
    Widget(Widget&& rhs); // 이동 생성자
    Widget& operator=(Widget&& rhs); // 이동 대입 연산자
};

C++11부터는 이동 생성자와 이동 대입 연산자까지 추가되어 총 6개의 특수 멤버 함수가 있다.

 

기본적으로 특수 멤버 함수는 사용자가 명시적으로 작성하지 않았지만 해당 함수를 사용하는 코드가 존재할때만 만들어진다고 했었다. 여기서 복사 연산과 이동 연산은 약간의 차이가 있다.

 

복사 생성자와 복사 대입 연산자는 서로 독립적이다. 복사 생성자가 명시적으로 선언되어 있고 복사 대입 연산자는 구현되어 있지 않은 상태에서 복사 대입 연산자를 사용한다면 컴파일러에 의해 복사 대입 연산자가 작성된다. 반대의 경우도 마찬가지이다.

 

하지만 이동 생성자와 이동 대입 연산자는 서로 종속적이다. 이동 생성자가 명시적으로 선언되어 있고 이동 대입 연산자는 구현되어 있지 않은 상태에서 이동 대입 연산자를 사용한다면 컴파일러는 복사 대입 연산자를 자동으로 작성하지 않는다. 반대의 경우도 마찬가지로 작성하지 않는다.

왜냐하면 하나라도 명시적으로 작성했다는 의미는 컴파일러가 기본적으로 작성해주는게 적합하지 않아서 사용자가 다른 방식으로 구현한 것이기 때문이다. 다른 한쪽을 컴파일러가 자동으로 작성해봤자 마찬가지로 적합하지 않을 가능성이 크기 때문에 한쪽이 명시적으로 선언되어 있다면 다른 한쪽은 자동으로 작성되지 않는다.

 

더 나아가서 복사 연산중 하나라도 명시적으로 선언되어있으면 이동 연산자는 자동으로 작성되지 않고 반대로 이동 연산자중 하나라도 명시적으로 선언되어있으면 복사 연산들은 삭제되어 비활성화된다.

 

3의 법칙

복사 생성자, 복사 대입 연산자, 소멸자 중 하나라도 선언했다면 나머지 둘도 선언해야 한다.

 

어떤 클래스의 복사 대입 연산을 직접 구현해야 하는 경우는 거의 항상 그 클래스가 자원 관리를 수행할 필요가 있기 때문이다. 구현한 복사 대입 연산이 수행하는 자원 관리 역시 다른 복사 연산에서도 수행해야 하고 소멸자도 그 자원의 관리에 참여해야 하기 때문에 셋 중 하나라도 선언했다면 나머지 둘도 선언해야 하는 것이다.

표준 라이브러리에서 메모리를 관리하는 모든 클래스가 2개의 복사 연산과 소멸자를 제공하는 이유가 이 때문이다.

사용자 선언 소멸자가 있는 클래스에서는 복사 연산들도 선언해야 하기 때문에 이동 연산들을 작성하지 않는다는 것을 추론할 수 있다.

 

정리하자면 다음과 같은 세 조건이 모두 만족할 때, 그리고 필요할 때에만 특수 멤버 함수가 자동으로 작성된다.

◾ 클래스에 그 어떤 복사 연산도 선언되어 있지 않다.

◾ 클래스에 그 어떤 이동 연산도 선언되어 있지 않다.

◾ 클래스에 소멸자가 선언되어 있지 않다.

 

class Widget {
public:
    ~Widget(); // 소멸자 선언. 복사 연산들도 선언해야 한다
    Widget(const Widget&) = default; // 기본 복사 생성자의 사용 의사를 밝힘
    Widget& operator(const Widget&) = default // 기본 복사 대입 연산자의 사용 의사를 밝힘
};

혹시라도 복사 연산, 소멸자 중 하나라도 작성한 상황에서 컴파일러가 자동으로 작성한 함수들의 행동이 정확하다면 C++11에서는 해당 함수들을 사용하겠다는 의사를 명시적으로 밝혀서 자동으로 작성된 코드를 사용할 수 있다.

 

만약 사용자 선언 소멸자를 두면서도 이동 연산을 지원하고 싶다면 이동 연산들에도 default를 지정하면 된다. 이 경우 복사 연산이 비활성화 되는데 복사 연산 역시 지원하고 싶다면 마찬가지로 default를 지정해준다.

 

class Base {
public:
    virtual ~Base() = default; // 가상 소멸자
    
    Base(Base&&) = default; // 이동 연산
    Base& operator=(Base&&) = default;
    
    Base(const Base&) = default; // 복사 연산
    Base& operator=(const Base&) = default;
};

 

특수 멤버 함수들을 관장하는 C++11의 규칙

기본 생성자

C++98의 규칙들과 같다. 클래스에 사용자 선언 생성자가 없는 경우에만 자동으로 작성된다.

 

소멸자

C++98의 규칙들과 같지만 기본적으로 noexcept라는 차이 하나만 존재한다.

기본적으로 작성되는 소멸자는 기반 클래스 소멸자가 가상일 때에만 가상이다.

 

복사 생성자

클래스에 사용자 선언 복사 생성자가 없을 때에만 자동으로 작성된다.

클래스에 이동 연산이 하나라도 존재하면 삭제된다.

 

복사 대입 연산자

클래스에 사용자 선언 복사 대입 연산자가 없을 떄에만 자동으로 작성된다.

클래스에 이동 연산이 하나라도 존재하면 삭제된다.

 

이동 생성자와 이동 대입 연산자

클래스에 사용자 선언 복사 연산들과 이동 연산들, 소멸자가 없을 때에만 자동으로 작성된다.

 

 

멤버 함수 템플릿이 존재한다고 해서 특수 멤버 함수의 자동 작성이 비활성화되지는 않는다.

 

◾ 컴파일러가 스스로 작성할 수 있는 멤버 함수들, 즉 기본 생성자와 소멸자, 복사 연산들, 이동 연산들을 가리켜 특수 멤버 함수라고 부른다.

◾ 이동 연산들은 이동 연산들이나 복사 연산들, 소멸자가 명시적으로 선언되어 있지 않은 클래스에 대해서만 자동으로 작성된다.

◾ 복사 생성자는 복사 생성자가 명시적으로 선언되어 있지 않은 클래스에 대해서만 자동으로 작성되며, 만일 이동 연산이 하나라도 선언되어 있으면 삭제된다. 소멸자가 명시적으로 선언된 클래스에서 복사 연산들이 자동 작성되는 기능은 비권장이다.

◾ 멤버 함수 템플릿 때문에 특수 멤버 함수의 자동 작성이 금지되는 경우는 전혀 없다.

+ Recent posts