C++ 以建構子與 std::swap 實作賦值運算子。

如果你的 class ,當中的 data member 擁有著動態記憶體,使用 compiler 提供的預設 Copy Control ,只會進行 shallow copy ,只抓其址,不抓其值。所造成的情況輕則 memory leaks,重則產生 dangling pointer。

所以當有以上的情形時,每一本 C++ 的書跟 Stack Overflow 上的回應都會要求你,一定要為這些 Data Member 寫 The Big Three ,也就是 Copy Constructor/ Copy Assignment/ Destructor,這三個管理成員生命週期的函式。

當你在寫的時候,會發現 Copy Constructor 與 Copy Assignment 的程式會有相當大程度的相似,但是他們情境卻是大大的不同。在 Copy Constructor 裡,函式內部的 object 當前狀態是 還未完整,要為每個成員做 初始化;而 Copy Assignment 中的情境則是,object 早已存在,現在要進行 覆寫其值。所以在 Copy Assignment 中的每一次 copy 前,都要先清理舊值,再做覆寫。於是乎就變成好像在做的事情差不多,卻難以抽出相似的程式碼。

以下的 class 範例,擁有著三組 data member(s),都是擁有著動態型別,所以必須在 Copy Control 中做小心的處理。可以看到在 Copy Constructor 與 Copy Assignment 中,看到很多相似的程式碼:

class Library
{public:
  // Data 1
  char* m_cstring;
  int m_cstring_length;
  
  // Data 2
  std::list<int> m_integer_list;
  std::list<int>::iterator m_integer_list_cursor;
  
  // Data 3
  std::vector<MyObject*> m_heap_object_vector;
  
  // Copy Control 1/3: Copy Constructor
  Library(const Library& library)
    :m_cstring(new char[library.m_cstring_length + 1]())
    ,m_cstring_length(library.m_cstring_length)
    ,m_integer_list(library.m_integer_list)
    ,m_integer_list_cursor(library.m_integer_list.end())
    ,m_heap_object_vector()
  {
    // Data 1
     strcpy_n(m_cstring, library.m_cstring_length + 1, library.m_cstring);
    
    // Data 2
    if(library.m_integer_list_cursor != library.m_integer_list.end())
    {
          m_integer_list_cursor = std::find(m_integer_list.begin(), 
                                                                            m_integer_list.end(), 
                                                                            *library.m_integer_list_cursor);
    }
    
    // Data 3
    m_heap_object_vector.reserve(library.m_heap_object_vector.size());
    for(const auto& obj: library.m_heap_object_vector)
    {
      m_heap_object_vector.push_back(new MyObject(obj));
    }
  }
  
  // Copy Control 2/3: Copy Assignment
  Library& operator=(const Library& library)
  {
    // Data 1
    delete [] m_cstring;
    m_cstring = new char[library.m_cstring_length + 1]();
        strcpy_n(m_cstring, library.m_cstring_length + 1, library.m_cstring);
    
    // Data 2
    m_integer_list = library.m_integer_list;
    if(library.m_integer_list_cursor != library.m_integer_list.end())
    {
          m_integer_list_cursor = std::find(m_integer_list.begin(), 
                                                                            m_integer_list.end(), 
                                                                            *library.m_integer_list_cursor);
    }
    
    // Data 3
    for(const auto& obj_to_delete: m_heap_object_vector)
    {
      delete obj_to_delete;
    }
    m_heap_object_vector.clear();
    m_heap_object_vector.reserve(library.m_heap_object_vector.size());
    for(const auto& obj: library.m_heap_object_vector)
    {
      m_heap_object_vector.push_back(new MyObject(obj));
    }
    return *this;
  }
  
  // Copy Control 3/3: Destructor
  ~Library()
  {
    // Data 1
    delete [] m_cstring;
    
    // Data 2
    // no need
   
    // Data 3
    for(const auto& obj_to_delete: m_heap_object_vector)
    {
      delete obj_to_delete;
    }    
  }
};

我們可以使用 制作一個複本,再將其成員一一 std::swap 的技術,來讓 Copy Assignment 盡量使用到 Copy Constructor 中的邏輯,而降低程式碼的重覆性。

class Library
{public:
    
  // Copy Control 1/3: Copy Constructor
  Library(const Library& library)
  {
    ...
  }   
  
  // Copy Control 2/3: Copy Assignment
  Library& operator=(const Library& library)
  {
    Library library_copy(library); 
    // 呼叫 library_copy.Library(const Library& library)
    std::swap(library_copy.m_cstring, m_cstring); // O(1)
    std::swap(library_copy.m_cstring_length, m_cstring_length); // O(1)
    std::swap(library_copy.m_integer_list, m_integer_list); // O(1)
    std::swap(library_copy.m_integer_list_cursor, m_integer_list_cursor); // O(1)
    std::swap(library_copy.m_heap_object_vector, m_heap_object_vector); // O(1)
    // 此 object 的值,皆已是參數 `library` 各成員的完整複本。
    // 舊值已被置換至 `library_copy`,等待離開此 Block 時,其 `~Library` 會為資源做清理
    return *this;     
  }// 呼叫 library_copy.~Library()
  
  // Copy Control 3/3: Destructor
  ~Library()
  {
    ...
  }
};

在 Herb Sutter 的 Exceptional C++ 中提到的這個方法,最大的目的是要提供 Exception Safe。就是當 copy assignment 中,遇到 exception,可能有一半的 data member 會被非預期地忽略而繞過解構程序,而讓 resource leak。
其當中的得到技術所得到額外好處,就是可以不用重覆你寫的程式碼。

p.s. 每一個 Class 應只管理一個動態資料。當有兩個動態資料需要管理,通常是一種 Code Smell。解決方法最好就是將他們分離成各別的 class。

ref: Exceptional C++ - http://www.amazon.com/Exceptional-Engineering-Programming-Problems-Solutions/dp/0201615622

comments powered by Disqus