軟件特攻隊|API 設計,C++不容忽視的錯誤設計(1)

軟件特攻隊|API 設計,C++不容忽視的錯誤設計(1)

對於許多C ++開發人員來說,API設計可能會在其優先級列表中排名第3或第4。大多數開發人員都傾向於使用C ++來獲得原始功能和控制權。因此,性能和優化的想法佔據這些開發者的時間的百分之八十。

當然,每個C ++開發人員都會考慮頭文件設計的各個方面,但是API設計不僅僅是頭文件設計那樣。事實上,我強烈建議每一個開發人員在其API的設計上,無論是面向公共還是面向內部,都給予一些幫助,因為這樣可以節省你大量的維護成本,提供平滑的升級路徑,併為你的客戶節省麻煩。

錯誤#1:不將你的API放在命名空間中

因為你不知道將使用哪個代碼庫,特別是對於外部API。如果不將API功能限制在命名空間中,則可能導致與該系統中使用的其他API發生名稱衝突。

例如:

讓我們考慮一個非常簡單的API和使用它的客戶端類:

//API - In Location.h

class vector

{

public:

vector(double x, double y, double z);

private:

double xCoordinate;

double yCoordinate;

double zCoordinate;

};

//Client Program

#include "stdafx.h"

#include "Location.h"

#include

using namespace std;

int main()

{

vector myVector;

myVector.push_back(99);

return 0;

}


如果有人試圖在同時使用std::vector的項目中使用這個類,他們會得到一個錯誤“error C2872: ‘vector’: ambiguous symbol”。這是因為編譯器無法決定客戶端代碼引用的向量是std::vector還是location.h中定義的vector對象。

如何解決這個問題?

始終將API放在自定義命名空間中,例如:

//API

namespace LocationAPI

{

class vector

{

public:

vector(double x, double y, double z);

private:

double xCoordinate;

double yCoordinate;

double zCoordinate;

};

}

另一種方法是為所有公共API符號添加一個唯一的前綴。如果遵循此約定,我們將調用我們的類“lvector”而不是“vector”。此方法用於OpenGL和QT。

在我看來,如果你正在開發純C的API,這是有道理的。確保所有公共符號符合此唯一命名約定是另一個令人頭痛的問題。如果你正在使用C ++,那麼你應該只在命名空間中對API功能進行分組,讓編譯器為你完成繁重的任務。

我還強烈建議你使用嵌套命名空間來進行功能分組或將公共API與內部API分開。一個很好的例子是Boost庫,它們可以自由地使用嵌套的命名空間。例如,在根“boost”命名空間內,boost :: variant包含Boost Variant API的公共符號,boost :: detail :: variant包含該API的內部詳細信息。

錯誤#2:在你的公共API頭的全局範圍中包含“using namespace”

這將導致被引用命名空間中的所有符號在全局命名空間中變得可見,並首先抵消掉使用命名空間的好處。

另外:

  • 頭文件的使用者不可能撤消命名空間包含,因此他們被迫使用決策來使用你的命名空間,這是不可取的。

  • 它極大地增加了命名空間首先要解決的衝突的可能性。

  • 當引入新版本的庫時,程序的工作版本可能無法編譯。如果新版本引入的名稱與應用程序正在從另一個庫使用的名稱衝突,則會發生這種情況。

  • 代碼中的“using namespace”部分從包含頭部的代碼中出現的那一點開始生效,這意味著在此之前出現的任何代碼都可能與該點之後出現的任何代碼區別對待。

如何解決這個問題?

1.儘量避免在頭文件中放置任何使用的命名空間聲明。如果你需要一些名稱空間對象來編頭文件,請在頭文件中使用完全限定名稱(例如std :: cout,std :: string)。

//File:MyHeader.h:

class MyClass

{

private:

Microsoft::WRL::ComPtr _parent;

Microsoft::WRL::ComPtr _child;

}

2.如果上面的建議#1導致代碼混亂太多 - 將“using namespace”用法限制在頭文件中定義的類或命名空間內。另一個選擇是在頭文件中使用範圍別名,如下所示。

//File:MyHeader.h:

class MyClass

{

namespace wrl = Microsoft::WRL; // note the aliasing here !

private:

wrl::ComPtr _parent;

wrl::ComPtr _child;

}

錯誤#3:無視“三法則”

什麼是“三法則”?

三法則是,如果一個類定義了析構函數、複製構造函數或複製賦值運算符,那麼它應該明確定義三個函數所有,而不是依賴它們的默認實現。

為什麼忽略三法則是一個錯誤?

如果你定義它們中的任何一個,很可能你的類正在管理一個資源(內存,fileHandle,套接字等)。從而:

  • 如果你編寫/禁用複製構造函數或複製賦值運算符,您可能需要對另一個執行相同操作:如果執行“special”工作,則另一個可能也應如此,因為這兩個函數應該具有相同的效果。

  • 如果你明確地編寫了複製函數,則可能需要編寫析構函數:如果複製構造函數中的“special”工作是分配或複製某些資源(例如,內存,文件,套接字等),則需要在其中釋放它析構函數。

  • 如果你明確地編寫了析構函數,則可能需要顯式寫入或禁用複製:如果必須編寫一個非常重要的析構函數,通常是因為你需要手動釋放該對象所持有的資源。如果是這樣,那些資源可能需要仔細複製,然後你需要注意對象的複製和分配方式,或者完全禁用複製。

讓我們看一個例子,在下面的API中,我們有一個由MyArray類管理的資源int *。我們為類創建了一個析構函數,因為我們知道在銷燬管理類時我們必須為int *釋放內存。到現在為止還挺好。

現在讓我們假設你的API的客戶端使用它如下所示。

int main()

{

int vals[4] = { 1, 2, 3, 4 };

MyArray a1(4, vals); // Object on stack - will call destructor once out of scope

MyArray a2(a1); // DANGER !!! - We're copyin the reference to the same object

return 0;

}


那麼這裡發生了什麼?

客戶端通過構造函數在eth堆棧上創建了類a1的實例。然後他通過從a1複製創建了另一個實例a2。當a1超出範圍時,析構函數將刪除底層int *的內存。但是當a2超出範圍時,它會再次調用析構函數並嘗試再次為int *釋放內存(此問題稱為雙重釋放),這會導致堆損壞。

由於我們沒有提供複製構造函數並且沒有將我們的API標記為不可複製,因此客戶端無法知道他不應該複製MyArray對象。

如何解決這個問題?

我們可以這樣一些事情:

  • 為創建底層資源的深層副本的類提供複製構造函數,例如(int *)就是這種情況。

  • 通過刪除複製構造函數和複製賦值運算符使類不可複製。

  • 最後,在API頭文件中提供該信息。

這是通過提供複製構造函數和複製賦值運算符來解決問題的代碼:

// File: RuleOfThree.h

class MyArray

{

private:

int size;

int* vals;

public:

~MyArray();

MyArray(int s, int* v);

MyArray(const MyArray& a); // Copy Constructor

MyArray& operator=(const MyArray& a); // Copy assignment operator

};

// Copy constructor

MyArray::MyArray(const MyArray &v)

{

size = v.size;

vals = new int[v.size];

std::copy(v.vals, v.vals + size, checked_array_iterator(vals, size));

}

// Copy Assignment operator

MyArray& MyArray::operator =(const MyArray &v)

{

if (&v != this)

{

size = v.size;

vals = new int[v.size];

std::copy(v.vals, v.vals + size, checked_array_iterator(vals, size));

}

return *this;

}


解決此問題的第二種方法是通過刪除複製構造函數和複製分配運算符使類不可複製。

// File: RuleOfThree.h

class MyArray

{

private:

int size;

int* vals;

public:

~MyArray();

MyArray(int s, int* v);

MyArray(const MyArray& a) = delete;

MyArray& operator=(const MyArray& a) = delete;

};



此時,當客戶端嘗試複製類時,他將遇到編譯錯誤:error C2280: ‘MyArray::MyArray(const MyArray &)’: attempting to reference a deleted function

C++ 11的附錄:

“三”法則現在已轉換為“五”法則,用於移動構造函數和移動賦值運算符中的因子。因此,在我們的例子中,如果要使類不可複製和不可移動,我們將標記移動構造函數和movbe賦值操作符為已刪除。

class MyArray

{

private:

int size;

int* vals;

public:

~MyArray();

MyArray(int s, int* v);

//The class is Non-Copyable

MyArray(const MyArray& a) = delete;

MyArray& operator=(const MyArray& a) = delete;

// The class is non-movable

MyArray(MyArray&& a) = delete;

MyArray& operator=(MyArray&& a) = delete;

};


附加警告:如果為類定義了複製構造函數(包括將其標記為已刪除),則不會為該類創建移動構造函數。因此,如果你的類只包含簡單的數據類型,並且你計劃使用隱式生成的移動構造函數,那麼如果你定義複製構造函數則不可能。在這種情況下時,你必須顯式定義移動構造函數。



分享到:


相關文章: