44

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

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

将其设为指针并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不仅仅是黑客?)。间接函数调用导致的性能差异可能已被优化掉。

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

4

12 回答 12

42

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

在这种情况下, 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() 函数可能没有意义。但我敢肯定,在您的实际情况下,这些函数所做的远比这里显示的要多,我上面的代码说明了语义设计的一种基本方法:让一个函数完成一项工作

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

于 2009-04-01T02:45:34.067 回答
29

我肯定会支持重载方法的第二种方法。

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

使用第二种方法(重载方法),每个方法都有明确的目的。每种方法都结构良好具有凝聚力。一些附加说明:

  • 如果有代码需要复制到两个方法中,可以将其提取到一个单独的方法中,每个重载的方法都可以调用这个外部方法。
  • 我会更进一步,以不同的方式命名每种方法,以表明这些方法之间的差异。这将使代码更具自我记录性。
于 2009-03-31T23:23:07.207 回答
21

虽然我确实理解许多人对默认参数和重载的抱怨,但似乎对这些功能提供的好处缺乏了解。

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

功能覆盖:功能覆盖 的好处是它们允许共享功能概念,但具有不同的选项/参数。然而,很多时候我看到函数重写被懒惰地用来提供截然不同的功能,只是参数略有不同。在这种情况下,它们每个都应该具有单独命名的函数,与它们的特定功能有关(与 OP 的示例一样)。

这些 c/c++ 的特性很好,如果使用得当,效果很好。这可以说是大多数编程功能。正是当它们被滥用/误用时,它们才会引起问题。

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

于 2012-02-20T23:06:54.110 回答
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()) {

   }
}
于 2012-11-28T17:27:28.403 回答
5

我同意,我会使用两个功能。基本上,你有两个不同的用例,所以有两个不同的实现是有意义的。

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

于 2009-03-31T23:22:43.337 回答
3

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

  • foo_default(或只是foo
  • foo_with_values

至少我发现这种区别在 long therm 中更清晰,对于偶尔的库/函数用户来说。

于 2009-03-31T23:23:10.873 回答
2

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

于 2009-03-31T23:22:10.813 回答
2

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

SomeFunction(NULL, pName, NULL, pDestination);

如果您将其切换为强制 const 引用,则代码只是被迫更具可读性。

SomeFunction(
  Location::Hidden(),
  pName,
  SomeOtherValue::Empty(),
  pDestination);
于 2009-04-01T01:13:27.330 回答
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 会解决这个问题)。

于 2009-07-02T10:25:31.697 回答
1

我赞成第三种选择:分成两个函数,但不要重载。

从本质上讲,重载不太可用。他们要求用户了解两个选项并弄清楚它们之间的区别,如果他们愿意,还要检查文档或代码以确保哪个是哪个。

我将有一个带有参数的函数,以及一个称为“createVectorAndFoo”或类似名称的函数(显然,在遇到实际问题时命名变得更容易)。

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

于 2009-03-31T23:49:33.930 回答
1

一般来说,我同意其他人使用双功能方法的建议。但是,如果在使用 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) {
    ...
}
于 2009-04-01T02:28:40.363 回答
0

第一种方法较差,因为您无法判断您是否不小心传入了 NULL 或者是否是故意这样做的……如果这是一个意外,那么您很可能导致了一个错误。

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

于 2009-03-31T23:37:20.353 回答