繁体   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