可选函数参数:使用默认参数(NULL)还是重载函数?
Posted
技术标签:
【中文标题】可选函数参数:使用默认参数(NULL)还是重载函数?【英文标题】:Optional function parameters: Use default arguments (NULL) or overload the function? 【发布时间】:2010-10-16 18:00:18 【问题描述】:我有一个处理给定向量的函数,但如果没有给出,也可以自己创建这样的向量。
对于这种情况,我看到了两种设计选择,其中函数参数是可选的:
将其设为指针并默认设为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 吗?)。并且间接函数调用导致的性能差异可能被优化掉了。
然而,我经常在代码中看到第一个解决方案。除了程序员的懒惰之外,还有令人信服的理由喜欢它吗?
【问题讨论】:
【参考方案1】:我不会使用任何一种方法。
在这种情况下, 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() 设计为获取引用而不是指针,调用者必须已经有一个向量是直观的,并且他们将很难传递一些不是现成向量的东西.
【讨论】:
这是最好的建议。首先,简化。我很惊讶地发现它位于答案堆栈的底部。 完全同意;我认为最好的答案 但是在版本 2 中创建向量在外部是不可见的 - 所以除了接受额外的参数之外,版本 1 和版本 2 的语义是相同的,不是他们?【参考方案2】:我绝对赞成第二种方法的重载方法。
第一种方法(可选参数)模糊了方法的定义,因为它不再有一个明确定义的目的。这反过来又增加了代码的复杂性,使不熟悉它的人更难理解。
使用第二种方法(重载方法),每种方法都有明确的目的。每种方法都结构良好和内聚。一些附加说明:
如果需要将代码复制到两个方法中,可以将其提取到单独的方法中,并且每个重载的方法都可以调用此外部方法。 我会更进一步,以不同的方式命名每个方法,以表明这些方法之间的差异。这将使代码更具自我记录性。【讨论】:
我对这个答案有点意见,但我不同意你的推理。重载的重点是每个方法都已经做了同样的事情,这就是为什么我们不关心通过名称来区分它们的原因。通常重载只会在它们接受的参数的 type 方面有所不同——想到一组print( int )
、print( double )
类型的函数。如果有一个可选的返回参数,那应该只是意味着调用者可能关心也可能不关心 rx 该可选结果。我同意这个答案的最后一句话:如果函数做不同的事情,那么是的,每个方法的名称不同。【参考方案3】:
虽然我确实理解许多人对默认参数和重载的抱怨,但似乎对这些功能提供的好处缺乏了解。
默认参数值: 首先我想指出,在项目的初始设计中,如果设计得当,默认值应该很少甚至没有用处。然而,默认值的最大优势在于现有项目和完善的 API。我从事的项目包含数百万行现有代码,并且没有机会重新编写所有代码。因此,当您希望添加需要额外参数的新功能时;新参数需要默认值。否则,您将破坏使用您项目的每个人。这对我个人来说没问题,但我怀疑你的公司或你的产品/API 的用户会喜欢在每次更新时重新编码他们的项目。 简单地说,默认值非常适合向后兼容!这通常是您在大型 API 或现有项目中看到默认值的原因。
函数覆盖: 功能覆盖的好处是它们允许共享功能概念,但具有不同的选项/参数。然而,很多时候我看到函数覆盖被懒惰地用来提供截然不同的功能,只是参数略有不同。在这种情况下,它们每个都应该有单独命名的函数,与它们的特定功能有关(与 OP 的示例一样)。
这些,c/c++ 的特性很好,如果使用得当,效果很好。这可以说是大多数编程功能。当它们被滥用/误用时,它们就会引起问题。
免责声明: 我知道这个问题已经有几年的历史了,但由于这些答案出现在我今天(2012 年)的搜索结果中,我觉得这需要为未来的读者进一步解决。
【讨论】:
【参考方案4】:在 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())
【讨论】:
也可以看看std::optional
。【参考方案5】:
我同意,我会使用两个函数。基本上,你有两个不同的用例,所以有两个不同的实现是有意义的。
我发现我编写的 C++ 代码越多,我的参数默认值就越少 - 如果该功能被弃用,我不会真的流泪,尽管我将不得不重新编写大量旧代码!
【讨论】:
完全同意第二段...默认参数(除了模板参数)几乎从来都不是一个好的设计选择。 除非它们真的是构造函数的唯一选择(无论如何在 C++'03 中)。【参考方案6】:我通常避免第一种情况。请注意,这两个功能的作用不同。其中一个用一些数据填充向量。另一个没有(只接受来自调用者的数据)。我倾向于命名实际上做不同事情的不同功能。实际上,即使您编写它们,它们也是两个函数:
foo_default
(或只是foo
)
foo_with_values
至少我发现这种区别在 long therm 中更清晰,对于偶尔的库/函数用户而言。
【讨论】:
【参考方案7】:我也更喜欢第二个。虽然两者之间没有太大区别,但您基本上使用 foo(int i)
重载中的主要方法的功能,并且主要重载可以完美地工作而无需关心是否存在缺少另一个重载,因此在重载版本中有更多的关注点分离。
【讨论】:
【参考方案8】:在 C++ 中,您应该尽可能避免允许使用有效的 NULL 参数。原因是它大大减少了调用站点文档。我知道这听起来很极端,但我使用的 API 需要超过 10-20 个参数,其中一半可以有效地为 NULL。生成的代码几乎不可读
SomeFunction(NULL, pName, NULL, pDestination);
如果您将其切换为强制 const 引用,则代码只会被强制变得更具可读性。
SomeFunction(
Location::Hidden(),
pName,
SomeOtherValue::Empty(),
pDestination);
【讨论】:
你是对的,将默认值命名(也许#defining它为0)比func( arg, 0, 0, 0, 0, 0, 0 );
更容易阅读【参考方案9】:
我完全属于“超载”阵营。其他人添加了有关您的实际代码示例的细节,但我想补充一下我认为在一般情况下使用重载与默认值相比的好处。
任何参数都可以“默认” 如果覆盖函数使用不同的默认值,则没有问题。 不必为现有类型添加“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 会解决这个问题)。
【讨论】:
【参考方案10】:我倾向于第三种选择: 分成两个函数,但不要重载。
从本质上讲,重载不太可用。它们要求用户了解两个选项并弄清楚它们之间的区别,如果他们愿意,还需要检查文档或代码以确保哪个是哪个。
我会有一个接受参数的函数, 还有一个叫做“createVectorAndFoo”或类似的东西(显然命名变得更容易解决实际问题)。
虽然这违反了“函数的两个职责”规则(并给它一个长名称),但我相信当你的函数确实做了两件事(创建向量和 foo 它)时,这更可取。
【讨论】:
我会反驳说,如果这个函数真的做了两件事,那么这是一个设计缺陷;应该修复的破窗(参见“实用程序员”)。【参考方案11】:一般来说,我同意其他人使用双功能方法的建议。但是,如果在使用 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)
...
【讨论】:
【参考方案12】:第一种方法比较差,因为你无法判断你是不小心传入了 NULL 还是故意这样做的......如果这是一个意外,那么你很可能导致了一个错误。
使用第二个,您可以测试(断言,无论如何)NULL 并适当地处理它。
【讨论】:
以上是关于可选函数参数:使用默认参数(NULL)还是重载函数?的主要内容,如果未能解决你的问题,请参考以下文章