[英]Use forwarded arguments in templated function to execute function with default parameters
[英]Optional function parameters: Use default arguments (NULL) or overload the function?
我有一個 function 可以處理給定的向量,但如果沒有給出,也可以自己創建這樣的向量。
對於這種情況,我看到了兩種設計選擇,其中 function 參數是可選的:
將其設為指針並默認NULL
:
void foo(int i, std::vector<int>* optional = NULL) {
if(optional == NULL){
optional = new std::vector<int>();
// fill vector with data
}
// process vector
}
或者有兩個具有重載名稱的函數,其中一個省略了參數:
void foo(int i) {
std::vector<int> vec;
// fill vec with data
foo(i, vec);
}
void foo(int i, const std::vector<int>& optional) {
// process vector
}
是否有理由更喜歡一種解決方案而不是另一種解決方案?
我稍微喜歡第二個,因為我可以將向量const
引用,因為它在提供時只能讀取,而不是寫入。 此外,界面看起來更干凈(不是NULL
只是一個 hack 嗎?)。 間接 function 調用導致的性能差異可能已被優化掉。
然而,我經常在代碼中看到第一個解決方案。 除了程序員的懶惰之外,還有令人信服的理由喜歡它嗎?
我不會使用任何一種方法。
在這種情況下, foo() 的目的似乎是處理一個向量。 也就是說,foo() 的工作是處理向量。
但在 foo() 的第二個版本中,它隱含地賦予了第二個工作:創建向量。 foo() 版本 1 和 foo() 版本 2 之間的語義不同。
如果你需要這樣的東西,我會考慮只使用一個 foo() function 來處理向量,以及另一個創建向量的 function ,而不是這樣做。
例如:
void foo(int i, const std::vector<int>& optional) {
// process vector
}
std::vector<int>* makeVector() {
return new std::vector<int>;
}
顯然這些函數是微不足道的,如果所有 makeVector() 需要做的只是調用 new 來完成它的工作,那么使用 makeVector() function 可能沒有意義。 但我敢肯定,在您的實際情況下,這些函數的作用遠不止此處所示,我上面的代碼說明了語義設計的一種基本方法:給 function 一個工作要做。
我上面對 foo() function 的設計還說明了我個人在代碼中使用的另一種基本方法,當涉及到設計接口時,它包括 function 簽名、類等。就是這樣:我相信一個好的接口是 1) 正確使用簡單直觀,以及 2) 難以或不可能錯誤使用。 在 foo() function 的情況下,我們含蓄地說,根據我的設計,向量必須已經存在並且“准備好”。 通過將 foo() 設計為獲取引用而不是指針,調用者必須已經有一個向量是直觀的,並且他們將很難傳遞一些不是現成向量的東西.
我肯定會支持重載方法的第二種方法。
第一種方法(可選參數)模糊了方法的定義,因為它不再有一個明確定義的目的。 這反過來又增加了代碼的復雜性,使不熟悉它的人更難理解它。
使用第二種方法(重載方法),每個方法都有明確的目的。 每種方法都結構良好且具有凝聚力。 一些附加說明:
雖然我確實理解許多人對默認參數和重載的抱怨,但似乎對這些功能提供的好處缺乏了解。
默認參數值:
首先我想指出,在項目的初始設計中,如果設計得當,默認值應該幾乎沒有用處。 然而,默認值的最大優勢在於現有項目和完善的 API。 我從事的項目包含數百萬行現有代碼,並且沒有機會重新編寫所有代碼。 因此,當您希望添加需要額外參數的新功能時; 新參數需要一個默認值。 否則,您將破壞使用您項目的每個人。 這對我個人來說沒問題,但我懷疑您的公司或您的產品/API 的用戶是否會喜歡在每次更新時重新編碼他們的項目。 簡單地說,默認值非常適合向后兼容! 這通常是您會在大型 API 或現有項目中看到默認設置的原因。
Function 覆蓋:function 覆蓋的好處是它們允許共享功能概念,但具有不同的選項/參數。 然而,很多時候我看到 function 覆蓋懶惰地用於提供截然不同的功能,只是參數略有不同。 在這種情況下,它們每個都應該具有單獨命名的函數,與它們的特定功能有關(與 OP 的示例一樣)。
這些 c/c++ 的特性很好,如果使用得當,效果很好。 這可以說是大多數編程功能。 正是當它們被濫用/誤用時,它們才會引起問題。
免責聲明:
我知道這個問題已經有幾年的歷史了,但由於這些答案出現在我今天(2012 年)的搜索結果中,我覺得這需要為未來的讀者進一步解決。
C++ 中的引用不能是 NULL,一個非常好的解決方案是使用 Nullable 模板。 這會讓你做的事情是 ref.isNull()
在這里你可以使用這個:
template<class T>
class Nullable {
public:
Nullable() {
m_set = false;
}
explicit
Nullable(T value) {
m_value = value;
m_set = true;
}
Nullable(const Nullable &src) {
m_set = src.m_set;
if(m_set)
m_value = src.m_value;
}
Nullable & operator =(const Nullable &RHS) {
m_set = RHS.m_set;
if(m_set)
m_value = RHS.m_value;
return *this;
}
bool operator ==(const Nullable &RHS) const {
if(!m_set && !RHS.m_set)
return true;
if(m_set != RHS.m_set)
return false;
return m_value == RHS.m_value;
}
bool operator !=(const Nullable &RHS) const {
return !operator==(RHS);
}
bool GetSet() const {
return m_set;
}
const T &GetValue() const {
return m_value;
}
T GetValueDefault(const T &defaultValue) const {
if(m_set)
return m_value;
return defaultValue;
}
void SetValue(const T &value) {
m_value = value;
m_set = true;
}
void Clear()
{
m_set = false;
}
private:
T m_value;
bool m_set;
};
現在你可以擁有
void foo(int i, Nullable<AnyClass> &optional = Nullable<AnyClass>()) {
//you can do
if(optional.isNull()) {
}
}
我同意,我會使用兩個功能。 基本上,你有兩個不同的用例,所以有兩個不同的實現是有意義的。
我發現我編寫的 C++ 代碼越多,我擁有的參數默認值就越少 - 如果該功能被棄用,我不會真的流淚,盡管我將不得不重新編寫大量舊代碼!
我通常避免第一種情況。 請注意,這兩個功能的作用不同。 其中一個用一些數據填充向量。 另一個沒有(只接受來自調用者的數據)。 我傾向於命名實際上做不同事情的不同功能。 事實上,即使在您編寫它們時,它們也是兩個函數:
foo_default
(或只是foo
)foo_with_values
至少我發現這種區別在 long therm 中更清晰,對於偶爾的庫/函數用戶來說。
我完全屬於“超載”陣營。 其他人添加了有關您的實際代碼示例的細節,但我想添加我認為在一般情況下使用重載與默認值的好處。
在每個上放置一些代碼示例:
可以默認任何參數:
class A {}; class B {}; class C {};
void foo (A const &, B const &, C const &);
inline void foo (A const & a, C const & c)
{
foo (a, B (), c); // 'B' defaulted
}
沒有覆蓋具有不同默認值的函數的危險:
class A {
public:
virtual void foo (int i = 0);
};
class B : public A {
public:
virtual void foo (int i = 100);
};
void bar (A & a)
{
a.foo (); // Always uses '0', no matter of dynamic type of 'a'
}
不必為現有類型添加“hacky”構造函數以允許它們被默認:
struct POD {
int i;
int j;
};
void foo (POD p); // Adding default (other than {0, 0})
// would require constructor to be added
inline void foo ()
{
POD p = { 1, 2 };
foo (p);
}
Output 參數可以默認而不需要使用指針或hacky全局對象:
void foo (int i, int & j); // Default requires global "dummy"
// or 'j' should be pointer.
inline void foo (int i)
{
int j;
foo (i, j);
}
規則重載與默認值的唯一例外是構造函數,構造函數當前無法轉發給另一個構造函數。 (我相信 C++ 0x 會解決這個問題)。
我也更喜歡第二個。 雖然兩者之間沒有太大區別,但您基本上是在foo(int i)
重載中使用主要方法的功能,並且主要重載可以完美地工作,而無需關心是否存在另一個重載,因此還有更多重載版本中的關注點分離。
在 C++ 中,您應該盡可能避免允許有效的 NULL 參數。 原因是它大大減少了調用站點文檔。 我知道這聽起來很極端,但我使用的 API 需要超過 10-20 個參數,其中一半可以是 NULL。 生成的代碼幾乎不可讀
SomeFunction(NULL, pName, NULL, pDestination);
如果您將其切換為強制 const 引用,則代碼只是被迫更具可讀性。
SomeFunction(
Location::Hidden(),
pName,
SomeOtherValue::Empty(),
pDestination);
我贊成第三種選擇:分成兩個函數,但不要重載。
從本質上講,重載不太可用。 他們要求用戶了解兩個選項並弄清楚它們之間的區別,如果他們願意,還要檢查文檔或代碼以確保哪個是哪個。
我會有一個 function 接受參數,還有一個叫做“createVectorAndFoo”或類似的東西(顯然命名變得更容易遇到實際問題)。
雖然這違反了“函數的兩個責任”規則(並給它一個長名稱),但我相信當您的 function 確實做了兩件事(創建向量和 foo 它)時,這更可取。
一般來說,我同意其他人使用雙功能方法的建議。 但是,如果在使用 1 參數形式時創建的向量始終相同,則可以通過改為 static 並使用默認的const&
參數來簡化事情:
// Either at global scope, or (better) inside a class
static vector<int> default_vector = populate_default_vector();
void foo(int i, std::vector<int> const& optional = default_vector) {
...
}
第一種方法較差,因為您無法判斷您是不小心通過了 NULL 還是故意這樣做……如果這是一次意外,那么您可能會導致錯誤。
使用第二個,您可以測試(斷言,無論如何) NULL 並適當地處理它。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.