簡體   English   中英

可選 function 參數:使用默認 arguments (NULL) 或過載 function?

[英]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() 設計為獲取引用而不是指針,調用者必須已經有一個向量是直觀的,並且他們將很難傳遞一些不是現成向量的東西.

我肯定會支持重載方法的第二種方法。

第一種方法(可選參數)模糊了方法的定義,因為它不再有一個明確定義的目的。 這反過來又增加了代碼的復雜性,使不熟悉它的人更難理解它。

使用第二種方法(重載方法),每個方法都有明確的目的。 每種方法都結構良好具有凝聚力 一些附加說明:

  • 如果有代碼需要復制到兩個方法中,可以將其提取到一個單獨的方法中,每個重載的方法都可以調用這個外部方法。
  • 我將 go 更進一步,並以不同的方式命名每種方法以指示方法之間的差異。 這將使代碼更具自我記錄性。

雖然我確實理解許多人對默認參數和重載的抱怨,但似乎對這些功能提供的好處缺乏了解。

默認參數值:
首先我想指出,在項目的初始設計中,如果設計得當,默認值應該幾乎沒有用處。 然而,默認值的最大優勢在於現有項目和完善的 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 中更清晰,對於偶爾的庫/函數用戶來說。

我完全屬於“超載”陣營。 其他人添加了有關您的實際代碼示例的細節,但我想添加我認為在一般情況下使用重載與默認值的好處。

  • 任何參數都可以“默認”
  • 如果覆蓋 function 對其默認值使用不同的值,則沒有問題。
  • 不必為現有類型添加“hacky”構造函數以允許它們具有默認值。
  • Output 參數可以默認,而無需使用指針或 hacky 全局對象。

在每個上放置一些代碼示例:

可以默認任何參數:

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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM