可选函数参数:使用默认参数(NULL)或重载函数?

时间:2009-03-31 23:15:20

标签: c++ function parameters null overloading

我有一个处理给定向量的函数,但如果没有给出,也可以自己创建这样的向量。

对于这种情况,我看到两种设计选择,其中函数参数是可选的:

将其设为指针并默认设为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只是一个黑客?)。并且间接函数调用产生的性能差异可能会被优化掉。

然而,我经常在代码中看到第一个解决方案。除了程序员的懒惰之外,是否有令人信服的理由更喜欢它?

12 个答案:

答案 0 :(得分:41)

我不会使用任何一种方法。

在这种情况下,foo()的目的似乎是处理一个向量。也就是说,foo()的工作是处理向量。

但是在foo()的第二个版本中,隐含地给出了第二个工作:创建向量。 foo()版本1和foo()版本2之间的语义不一样。

如果你需要这样的东西,我会考虑只使用一个foo()函数来处理一个向量,而另一个函数创建向量,而不是这样做。

例如:

void foo(int i, const std::vector<int>& optional) {
  // process vector
}

std::vector<int>* makeVector() {
   return new std::vector<int>;
}

显然这些函数是微不足道的,如果所有makeVector()需要做的就是完成它的工作就完全是调用new,那么使用makeVector()函数可能没有意义。但我确信在你的实际情况中,这些函数比这里显示的更多,而我上面的代码说明了语义设计的基本方法:给一个函数做一个工作。 / p>

我上面提到的foo()函数的设计也说明了我个人在设计接口时使用的另一种基本方法 - 包括函数签名,类等。这就是:我相信良好的界面1)简单直观,正确使用,2)难以或不可能错误使用。在foo()函数的情况下,我们隐含地说,根据我的设计,向量必须已经存在并且已经“准备好”。通过设计foo()来获取引用而不是指针,调用者必须已经有一个向量是直观的,并且他们将很难传递一些不是现成的向量

答案 1 :(得分:28)

我绝对赞成重载方法的第二种方法。

第一种方法(可选参数)模糊了方法的定义,因为它不再具有明确定义的目的。这反过来会增加代码的复杂性,使不熟悉它的人更难理解它。

使用第二种方法(重载方法),每种方法都有明确的目的。每种方法都是结构良好的内聚。一些补充说明:

  • 如果需要将代码复制到两个方法中,可以将提取到单独的方法中,并且每个重载方法都可以调用此外部方法。
  • 我会更进一步,以不同方式命名每个方法以指示方法之间的差异。这将使代码更加自我记录。

答案 2 :(得分:21)

虽然我理解许多人对默认参数和过载的抱怨,但似乎对这些功能所带来的好处缺乏了解。

默认参数值:
首先,我想指出,在项目的初始设计中,如果设计得当,应该几乎没有用于默认设置。但是,默认情况下,最大的资产发挥作用的是现有项目和完善的API。我从事包含数百万现有代码行的项目,并且无需重新编写所有代码。因此,当您希望添加需要额外参数的新功能时;新参数需要默认值。否则,您将破坏使用您项目的每个人。哪个对我个人没问题,但我怀疑你的公司或产品/ API的用户会喜欢在每次更新时重新编码他们的项目。 简单地说,默认值非常适合向后兼容!这通常是您在大型API或现有项目中看到默认值的原因。

功能覆盖: 功能覆盖的好处是它们允许共享功能概念,但具有不同的选项/参数。但是,很多时候我看到函数覆盖延迟用于提供截然不同的功能,只是略有不同的参数。在这种情况下,它们应该各自具有与其特定功能相关的单独命名的功能(与OP的示例一样)。

这些c / c ++的功能很好,并且在正确使用时效果很好。这可以说是大多数编程功能。当他们被滥用/滥用时,他们会引起问题。

<强>声明:
我知道这个问题已有几年了,但由于今天(2012年)我的搜索结果中出现了这些答案,我觉得这需要进一步解决未来的读者问题。

答案 3 :(得分:6)

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()) {

   }
}

答案 4 :(得分:5)

我同意,我会使用两个功能。基本上,您有两种不同的用例,因此有两种不同的实现是有意义的。

我发现我编写的C ++代码越多,我的参数默认值就越少 - 如果该功能被弃用,我真的不会流泪,尽管我不得不重新编写一堆旧代码! / p>

答案 5 :(得分:3)

我通常会避免第一种情况。请注意,这两个功能的不同之处在于它们的功能。其中一个用一些数据填充矢量。另一个不(只接受来自呼叫者的数据)。我倾向于命名不同的功能,实际上做不同的事情。事实上,即使你写它们,它们也有两个功能:

  • foo_default(或仅foo
  • foo_with_values

至少我发现这个区别在long therm和偶尔的库/函数用户中更清晰。

答案 6 :(得分:2)

我也喜欢第二个。虽然两者之间没有太大区别,但基本上使用 foo(int i)重载中的主要方法的功能,并且主要重载将完美地工作而不关心是否存在缺少另一个,所以在重载版本中有更多的关注点分离。

答案 7 :(得分:2)

在C ++中,应尽可能避免允许有效的NULL参数。原因是它大大减少了呼叫站点文档。我知道这听起来很极端,但我使用的API使用了10-20个参数,其中一半可以有效地为NULL。生成的代码几乎不可读

SomeFunction(NULL, pName, NULL, pDestination);

如果要将其切换为强制const引用,则只需强制代码更具可读性。

SomeFunction(
  Location::Hidden(),
  pName,
  SomeOtherValue::Empty(),
  pDestination);

答案 8 :(得分:2)

我正处于“超载”阵营。其他人已经添加了有关您的实际代码示例的细节,但我想添加我认为使用重载与一般情况下的默认值的好处。

  • 任何参数都可以“默认”
  • 如果覆盖函数的默认值使用不同的值,则无效。
  • 没有必要为现有类型添加“hacky”构造函数,以允许它们具有默认值。
  • 可以默认输出参数,而无需使用指针或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);
}

可以默认输出参数,而无需使用指针或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会解决这个问题。)

答案 9 :(得分:1)

我赞成第三种选择: 分为两个功能,但不要过载。

超载本质上不太可用。它们要求用户了解两个选项并找出它们之间的区别,如果它们如此倾向,还要检查文档或代码以确保哪个选项。

我会有一个带参数的函数, 还有一个叫做“createVectorAndFoo”的东西(显然命名变得容易实现真正的问题)。

虽然这违反了“功能的两个责任”规则(并给它一个长名称),但我相信当你的函数确实做了两件事(创建向量和foo)时,这是更好的。

答案 10 :(得分:1)

一般来说,我同意其他人使用双功能方法的建议。但是,如果在使用单参数表单时创建的向量始终相同,则可以通过改为使其静态并使用默认的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) {
    ...
}

答案 11 :(得分:0)

第一种方式比较差,因为你无法判断你是否意外地传递了NULL,或者它是否是故意的......如果是偶然的话,你可能会造成错误。

使用第二个,您可以测试(断言,无论如何)NULL并适当地处理它。