객체 수명 불변량 대 이동 의미론


13

C ++을 오래 ​​전에 배웠을 때, C ++의 요점 중 하나는 루프에 "루프 불변"이있는 것처럼 클래스에도 객체의 수명과 관련된 불변이 있다는 것입니다. 개체가 살아있는 동안 생성자에 의해 확립되고 메소드에 의해 보존되어야하는 것들. 인 캡슐 레이션 / 액세스 제어는 변하지 않는 것을 적용하는 데 도움이됩니다. RAII는이 아이디어로 할 수있는 일입니다.

C ++ 11부터 이제 이동 의미론이 있습니다. 이동을 지원하는 클래스의 경우 객체에서 이동해도 수명이 공식적으로 종료되지 않습니다. 이동은 "유효한"상태로 유지됩니다.

클래스 를 디자인 할 때 클래스의 불변이 클래스가 이동하는 지점까지만 보존되도록 디자인하면 나쁜 습관 입니까? 또는 더 빨리 갈 수 있다면 괜찮 습니다.

구체적으로, 복사 할 수 없지만 이동 가능한 자원 유형이 있다고 가정하십시오.

class opaque {
  opaque(const opaque &) = delete;

public:
  opaque(opaque &&);

  ...

  void mysterious();
  void mysterious(int);
  void mysterious(std::vector<std::string>);
};

그리고 어떤 이유로 든이 객체에 대해 복사 가능한 래퍼를 만들어야 기존의 일부 디스패치 시스템에서 사용할 수 있습니다.

class copyable_opaque {
  std::shared_ptr<opaque> o_;

  copyable_opaque() = delete;
public:
  explicit copyable_opaque(opaque _o)
    : o_(std::make_shared<opaque>(std::move(_o)))
  {}

  void operator()() { o_->mysterious(); }
  void operator()(int i) { o_->mysterious(i); }
  void operator()(std::vector<std::string> v) { o_->mysterious(v); }
};

copyable_opaque객체에서, 구축시에 설정된 클래스의 불변은 o_기본 ctor가 없기 때문에 멤버가 항상 유효한 객체를 가리키고 복사 ctor가 아닌 유일한 ctor가이를 보장한다는 것입니다. 모든 operator()방법은이 불변이 유지되는 것으로 가정하고 나중에 유지합니다.

그러나 객체가 이동 o_하면 아무 것도 가리 키지 않습니다. 그리고 그 후에 메소드를 호출 operator()하면 UB / 충돌이 발생합니다.

객체가 이동되지 않은 경우에는 고정자가 dtor 호출까지 그대로 유지됩니다.

가설 적으로, 나는이 수업을 썼고, 몇 달 후, 나의 가상의 동료가 UB를 경험했다고 가정 해 봅시다. 왜냐하면 이러한 많은 객체들이 어떤 이유로 셔플되는 복잡한 기능에서, 그는 이런 것들 중 하나에서 옮겨져 나중에 그 방법. 하루가 끝났을 때 분명히 그의 잘못 이었지만이 수업은 "나쁘게 설계 되었습니까?"

생각 :

  1. 좀비를 만지면 폭발하는 좀비 객체를 만드는 것은 일반적으로 C ++에서 좋지 않은 형태입니다.
    어떤 객체를 만들 수 없다면 불변 값을 설정할 수 없으며 ctor에서 예외를 던집니다. 어떤 방법으로 불변 값을 보존 할 수 없으면 어떻게 든 오류를 표시하고 롤백합니다. 이동 한 객체와 다른 점이 있습니까?

  2. 헤더에 "이 오브젝트가 이동 된 후 그것을 파괴하는 것 외에 다른 것을하는 것은 불법 (UB)"이라고 문서화하는 것으로 충분합니까?

  3. 각 메소드 호출에서 유효하다고 계속 주장하는 것이 더 낫습니까?

이렇게 :

class copyable_opaque {
  std::shared_ptr<opaque> o_;

  copyable_opaque() = delete;
public:
  explicit copyable_opaque(opaque _o)
    : o_(std::make_shared<opaque>(std::move(_o)))
  {}

  void operator()() { assert(o_); o_->mysterious(); }
  void operator()(int i) { assert(o_); o_->mysterious(i); }
  void operator()(std::vector<std::string> v) { assert(o_); o_->mysterious(v); }
};

단언은 행동을 실질적으로 개선하지 않으며 속도 저하를 유발합니다. 프로젝트가 항상 어설 션으로 실행하는 대신 "릴리스 빌드 / 디버그 빌드"체계를 사용하는 경우 릴리스 빌드의 검사 비용을 지불하지 않기 때문에 이것이 더 매력적이라고 ​​생각합니다. 실제로 디버그 빌드가 없다면, 이것은 꽤 매력적이지 않은 것 같습니다.

  1. 클래스를 복사 가능하지만 이동 가능하게 만드는 것이 더 낫습니까?
    이 또한 나쁘게 보이고 성능 저하를 유발하지만 "불변"문제를 간단하게 해결합니다.

여기서 관련 "모범 사례"로 고려할 사항은 무엇입니까?



답변:


20

좀비를 만지면 폭발하는 좀비 객체를 만드는 것은 일반적으로 C ++에서 좋지 않은 형태입니다.

그러나 그것은 당신이하고있는 것이 아닙니다. 잘못 터치하면 폭발하는 "좀비 오브젝트"를 생성하고 있습니다 . 이는 궁극적으로 다른 국가 기반 전제 조건과 다르지 않습니다.

다음 기능을 고려하십시오.

void func(std::vector<int> &v)
{
  v[0] = 5;
}

이 기능은 안전합니까? 아니; 사용자는 전달할 수 있습니다 vector. 따라서 함수에는 사실상 v하나 이상의 요소가있는 사실상 전제 조건이 있습니다. 그렇지 않으면을 호출 할 때 UB를 얻습니다 func.

따라서이 기능은 "안전하지 않습니다". 그러나 이것이 고장났다는 의미는 아닙니다. 코드를 사용하는 코드가 전제 조건을 위반하는 경우에만 손상됩니다. 어쩌면 func다른 기능의 구현에 도우미로 사용되는 정적 기능입니다. 그런 식으로 현지화되어 있으면 아무도 전제 조건을 위반하는 방식으로 부르지 않습니다.

네임 스페이스 범위 또는 클래스 멤버에 관계없이 많은 함수는 작동하는 값의 상태에 대한 기대치를 갖습니다. 이러한 전제 조건이 충족되지 않으면 일반적으로 UB에서 기능이 실패합니다.

C ++ 표준 라이브러리는 "유효하지만 지정되지 않은"규칙을 정의합니다. 표준에서 달리 언급하지 않는 한, 이동 된 모든 객체는 유효하지만 (해당 유형의 유효한 객체) 해당 객체의 특정 상태 는 지정되지 않습니다. 이사 한 요소는 몇 개 vector입니까? 말하지 않습니다.

이것은 전제 조건 이 있는 함수를 호출 할 수 없음을 의미합니다 . vector::operator[]vector하나 이상의 요소가 있다는 전제 조건이 있습니다. 의 상태를 vector모르므로 호출 할 수 없습니다. 비어 있지 않은지 func먼저 확인하지 않고 호출하는 것보다 낫지 vector않습니다.

그러나 이것은 또한 전제 조건이 없는 기능 이 훌륭 하다는 것을 의미 합니다. 이것은 완벽하게 합법적 인 C ++ 11 코드입니다.

vector<int> v1 = {1, 2, 3, 4, 5};
vector<int> v2{std::move(v1)};
v1.assign({6, 7, 8, 9, 10});

vector::assign전제 조건이 없습니다. vector이동 한 객체 라도 모든 유효한 객체 와 함께 작동합니다 .

따라서 깨진 개체를 만들지 않습니다. 상태를 알 수없는 객체를 생성 중입니다.

어떤 객체를 만들 수 없다면 불변 값을 설정할 수 없으며 ctor에서 예외를 throw하십시오. 어떤 방법으로 불변량을 보존 할 수 없다면 어떻게 든 오류를 알리고 롤백하십시오. 이동 된 오브젝트와 다른 점이 있습니까?

이동 생성자에서 예외를 던지는 것은 일반적으로 무례한 것으로 간주됩니다. 메모리를 소유 한 객체를 이동하면 해당 메모리의 소유권을 이전하게됩니다. 그리고 그것은 일반적으로 던질 수있는 것을 포함하지 않습니다.

안타깝게도 우리는 여러 가지 이유로 이것을 시행 할 수 없습니다 . 우리는 던지기 이동이 가능하다는 것을 인정해야합니다.

"아직 지정되지 않은"언어를 따를 필요는 없습니다. 이것이 C ++ 표준 라이브러리가 표준 유형에 대한 이동 이 기본적으로 작동한다고 말하는 방식 입니다. 특정 표준 라이브러리 유형은보다 엄격한 보증을 제공합니다. 예를 들어 unique_ptr이동 한 unique_ptr인스턴스 의 상태에 대해 매우 명확 합니다 nullptr.

원하는 경우 더 강력한 보증을 제공하도록 선택할 수 있습니다.

운동은 성능 최적화 이며, 대개 파괴 하려고 하는 물체에서 수행됩니다 . 이 코드를 고려하십시오.

vector<int> func()
{
  vector<int> v;
  //fill up `v`.
  return v;
}

이것은 v컴파일러가 그것을 제거하지 않는다고 가정 할 때 반환 값으로 이동 합니다. 그리고 이동이 완료된 후 참조 할 방법없습니다v . 따라서 v유용한 상태로 만들기 위해 수행 한 모든 작업 은 의미가 없습니다.

대부분의 코드에서 이동 된 객체 인스턴스를 사용할 가능성은 낮습니다.

헤더에 "이 오브젝트가 이동 된 후 그것을 파괴하는 것 외에 다른 것을하는 것은 불법 (UB)"이라고 문서화하는 것으로 충분합니까?

각 메소드 호출에서 유효하다고 계속 주장하는 것이 더 낫습니까?

전제 조건을 갖는 요점은 그러한 것들을 확인 하지 않는 것입니다. 주어진 색인을 가진 요소가 있어야 operator[]한다는 전제 조건이 vector있습니다. 의 크기 외부에 액세스하려고하면 UB가 표시됩니다 vector. 그러한 전제 조건 vector::at 이 없습니다 . vector값이없는 경우 명시 적으로 예외가 발생 합니다.

성능상의 이유로 전제 조건이 존재합니다. 발신자가 직접 확인할 수있는 사항을 확인할 필요가 없습니다. 모든 전화 가 비어 v[0]있는지 확인할 필요 v는 없습니다 . 첫 번째 것만.

클래스를 복사 가능하지만 이동 가능하게 만드는 것이 더 낫습니까?

사실, 수업은 절대로 "복사 가능하지만 움직일 수" 없어야합니다 . 복사 할 수 있으면 복사 생성자를 호출하여 이동할 수 있어야합니다. 사용자 정의 복사 생성자를 선언했지만 이동 생성자를 선언하지 않은 경우 C ++ 11의 표준 동작입니다. 특수 이동 의미론을 구현하지 않으려는 경우 채택해야하는 동작입니다.

이동 시맨틱은 매우 특정한 문제를 해결하기 위해 존재합니다. 복사가 엄청나게 비싸거나 의미가없는 (즉, 파일 핸들) 자원이 많은 개체를 처리하는 것입니다. 대상이 적합하지 않으면 복사 및 이동이 동일합니다.


5
좋은. +1. "전제 조건을 갖는 모든 요점은 그러한 것들을 확인하지 않는 것입니다." -나는 이것이 주장을지지한다고 생각하지 않는다. 주장은 이럴 체크 전제 조건에 좋은 유효한 도구입니다 (적어도 대부분의 시간.)
마틴 바

3
이동 ctor가 새 객체와 동일한 것을 포함하여 어떤 상태로든 소스 객체를 떠날 수 있음을 인식함으로써 복사 / 이동 혼란을 명확히 할 수 있습니다 .
MSalters
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.