5

C++에서 클래스에 동적으로 할당 된 데이터가 포함되어 있으면 일반적으로 복사 생성자 인 operator = 및 destructor를 명시 적으로 정의하는 것이 타당합니다. 그러나 이러한 특별한 방법의 활동은 겹칩니다. 좀 더 구체적으로 operator = 일반적으로 먼저 복사 작업을 수행 한 다음 복사 생성자에서와 비슷한 대처법을 사용합니다.복사 생성자와 연산자에서 동일한 코드를 반복하지 마십시오. operator =

내 질문은 동일한 코드 줄을 반복하지 않고 프로세서가 불필요한 작업 (예 : 불필요한 복사)을하지 않아도 최상의 방법을 쓰는 방법입니다.

나는 보통 두 가지 방법으로 끝납니다. 하나는 건설 용이고 다른 하나는 파괴 용입니다. 첫 번째는 복사 생성자 및 연산자 = 모두에서 호출됩니다. 두 번째는 소멸자와 연산자 =에 의해 사용됩니다.

template <class T> 
    class MyClass 
    { 
     private: 
     // Data members 
     int count; 
     T* data; // Some of them are dynamicly allocated 
     void construct(const MyClass& myClass) 
     { 
      // Code which does deep copy 
      this->count = myClass.count; 
      data = new T[count]; 
      try 
      { 
       for (int i = 0; i < count; i++) 
        data[i] = myClass.data[i]; 
      } 
      catch (...) 
      { 
       delete[] data; 
       throw; 
      } 
     } 
     void destruct() 
     { 
      // Dealocate all dynamicly allocated data members 
      delete[] data; 
     } 
     public: MyClass(int count) : count(count) 
     { 
      data = new T[count]; 
     } 
     MyClass(const MyClass& myClass) 
     { 
      construct(myClass); 
     } 
     MyClass& operator = (const MyClass& myClass) 
     { 
      if (this != &myClass) 
      { 
       destruct(); 
       construct(myClass); 
      } 
      return *this; 
     } 
     ~MyClass() 
     { 
      destruct(); 
     } 
    }; 

이 더욱 정확 : 여기

은 예제 코드? 그런 식으로 코드를 분할하는 것이 좋은 습관입니까?

+0

+1 내 질문에 도움이 되었기 때문에 +1. 나는 답을 읽기 전에 내가 썼을 무언가처럼 보입니다. – ToolmakerSteve

+0

흠, 둘 다 코드가 중복되는 경우는 거의 없습니다. 둘 다 완전히 다른 것들이 있습니다. 하나는 초기화하고, 하나는 .... – PlasmaHH

+0

은 복제로 이어지는 그의 클래스 디자인의 "딥 카피"특성을 지정합니다. – ToolmakerSteve

답변

0

먼저 오른쪽면을 복사 한 다음 스와핑하여 할당을 구현하십시오. 이렇게하면 위의 코드에서 제공하지 않는 예외 안전성을 얻을 수 있습니다. 멤버 포인터가 일부 할당 해제 된 데이터를 참조하고, 다시 할당을 취소하여 정의되지 않은 동작을 일으키기 때문에 destroy()가 성공한 후에 construct()가 실패 할 때 깨진 컨테이너로 끝날 수 있습니다. 당신이 구조를 선언하거나 가상 소멸하지 않도록하기로

foo& 
foo::operator=(foo const& rhs) 
{ 
    using std::swap; 
    foo tmp(rhs); 
    swap(*this, tmp); 
    return *this; 
} 
+0

구조가 실패하면 왜 더 오래된) 내용 대신 빈 컨테이너를 사용 하시겠습니까? 빈 컨테이너에 실패한 복사 결과가있는 것이 더 깔끔합니다. 특히 빈 컨테이너는 이후 코드에서 쉽게 감지 할 수 있습니다. 내용이 더 이상없는 용기는 나중에 감지하기가 더 어렵습니다. – ToolmakerSteve

+0

그런 실패의 경우에 프로그램을 종료하고 싶다고 가정하면 어느 쪽이든 정상적으로 작동하지만'data = nullptr' (원래 코드가 실패한)을 설정해야합니다. – riv

+0

@ToolmakerSteve 그것은 당신이 원하는 것에 달려 있습니다. 스왑 이디엄은 완전한 트랜잭션 무결성을 보증합니다. 당신은 성공하거나 아무것도 변하지 않습니다. 대부분의 경우 개별 개체에 대해 완전한 트랜잭션 무결성이 필요하지 않습니다. 당신이 보증해야하는 모든 것은 실패한 경우 객체가 파괴 될 수 있다는 것입니다. (실제 상태가 변경되지 않은 것을 보증하려고 시도하는 것은별로 유용하지 않을 것입니다.) –

0

나는 한, 그 어떤 고유 한 문제가 표시되지 않습니다.

Effective C++ (Scott Meyers)의 2 장에 관심이있을 것입니다.이 생성기는 생성자, 복사 연산자 및 소멸자에 전적으로 사용됩니다.

코드가 처리해야하는 예외가있는 경우 항목 10 & 11을 더 효과적인 C++ (Scott Meyers)에서 고려하십시오.

+1

예외는 아닙니다. 'construct'의'new'가 던지면 (복사가 던지면), 객체는 비 일관적인 상태로 남게됩니다.이 상태에서 그것을 파괴하면 정의되지 않은 동작이 발생할 것입니다. –

+0

@JamesKanze 물론 당신은 맞습니다.하지만 문제는 코드 중복을 피하는 기술에 관한 것이고,이 기술에는 고유 한 문제가 없다고 생각합니다. –

+0

고유 한 문제가 없으며 _except_가 작동하지 않습니다. 예외 안전은 선택적 기능이 아닙니다. 프로그램이 올바른 것이면 필수적입니다. –

5

초기 설명이 하나 : operator=이 아니며은 시작하여 으로 시작합니다. 그렇지 않은 경우 예외를 통해 구성이 종료되면 개체가 잘못된 상태가됩니다. 이 때문에 귀하의 코드가 올바르지 않습니다. (할당 연산자가 올바른하지 있다는 신호가 일반적입니다 에게 자기 과제에 대한 테스트의 필요성을 참고.)

이 처리를위한 고전적인 솔루션은 스왑 관용구이다 : 당신이 멤버 함수 스왑을 추가 :

void MyClass:swap(MyClass& other) 
{ 
    std::swap(count, other.count); 
    std::swap(data, other.data); 
} 

. 그런 다음 당신은 할당 연산자를 구현 (. 여기에, 그냥 던질 수도있는 지능 및 포인터를 스왑) :

MyClass& MyClass<T>::operator=(MyClass const& other) 
{ 
    MyClass tmp(other); 
    swap(tmp); 
    return *this; 
} 

이 똑바로 앞으로 간단하고, 그러나 어떤 솔루션있는 모든 시작하기 전에 실패 할 수있는 작업이 완료되면 데이터를 변경할 수 있습니다.예를 들어 당신의 코드 같은 간단한 경우를 들어

MyClass& MyClass<T>::operator=(MyClass const& other) 
{ 
    T* newData = cloneData(other.data, other.count); 
    delete data; 
    count = other.count; 
    data = newData; 
    return *this; 
} 

(cloneData는 않는 멤버 함수입니다 귀하의 construct를 않습니다 하지만 포인터를 반환하고, this에 아무것도 수정하지 무엇을 대부분).

편집 :

직접 초기 문제와 관련,하지만 일반적으로 이러한 경우에, 당신은 하지new T[count] (어떤 또는 construct, 또는) cloneData 에하고 싶어 할 수 없습니다. 그러면 T의 이 모두 기본 생성자와 함께 생성 된 다음 할당됩니다. 이 일의 관용적 방법은 같은 :

// Inside MyClass, private: 
struct Data 
{ 
    T* data; 
    int count; 
    Data(int count) 
     : data(static_cast<T*>(operator new(count * sizeof(T))) 
     , count(0) 
    { 
    } 
    ~Data() 
    { 
     while (count != 0) { 
      -- count; 
      (data + count)->~T(); 
     } 
    } 
    void swap(Data& other) 
    { 
     std::swap(data, other.data); 
     std::swap(count, other.count); 
    } 
}; 
Data data; 

// Copy constructor 
MyClass(MyClass const& other) 
    : data(other.data.count) 
{ 
    while (data.count != other.data.count) { 
     new (data.data + data.count) T(other.date[data.count]); 
     ++ data.count; 
    } 
} 

(물론, 스왑 관용구 :

대부분의 경우
T* 
MyClass<T>::cloneData(T const* other, int count) 
{ 
    // ATTENTION! the type is a lie, at least for the moment! 
    T* results = static_cast<T*>(operator new(count * sizeof(T))); 
    int i = 0; 
    try { 
     while (i != count) { 
      new (results + i) T(other[i]); 
      ++ i; 
     } 
    } catch (...) { 
     while (i != 0) { 
      -- i; 
      results[i].~T(); 
     } 
     throw; 
    } 
    return results; 
} 

, 이것은 별도의 (개인) 관리자에게 클래스를 사용하여 수행됩니다 할당). 이로 인해 여러 건의 카운트/데이터 쌍이 있어도 안전을 잃지 않고 아무런 위험이 없습니다.

+0

+1, 고맙습니다 @ 제임스,이 주제를 더 잘 이해했습니다. – ToolmakerSteve

+0

이것은 하나 이상의 가치가있는 혁신적인 것입니다. "- 자기 할당을 테스트 할 필요성은 일반적으로 할당 연산자가 올바르지 않다는 신호입니다." – SChepurin

+0

@ James Kanze : 직장 동료가 만난 한 가지 경우는 할당 연산자가 리소스 중 하나에서 memcpy를 수행해야하는 경우입니다. 이 경우 자체 할당이 필요합니다. – ForeverLearning