可选函数参数:使用默认参数(NULL)还是重载函数?
我有一个处理给定vector的函数,但是如果没有给出,也可以创build这样一个vector。
在这种情况下,我看到两个devise选项,其中函数参数是可选的:
使它成为一个指针,并使其默认为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 }
有理由相对于另一个更喜欢一个解决scheme吗?
我稍微喜欢第二个,因为我可以使向量为const
引用,因为它提供时只能读,而不能写。 此外,界面看起来更清洁(不是NULL
只是一个黑客?)。 由间接函数调用产生的性能差异可能被优化掉了。
然而,我经常在代码中看到第一个解决scheme。 除了程序员的懒惰之外,是否有令人信服的理由去select呢?
我肯定会赞成重载方法的第二种方法。
第一种方法( 可选参数 )模糊了方法的定义,因为它不再有一个明确定义的目的。 这反过来增加了代码的复杂性 ,使不熟悉它的人更难理解它。
用第二种方法( 重载方法 ),每种方法都有明确的目的。 每种方法都是结构合理和有凝聚力的 。 一些附加说明:
- 如果需要将代码复制到两个方法中,则可以将这些代码抽取到单独的方法中,并且每个重载的方法都可以调用此外部方法。
- 我会更进一步,以不同的方式命名每种方法,以指示方法之间的差异。 这将使代码更加自我logging。
我不会使用任何一种方法。
在这种情况下,foo()的目的似乎是处理一个向量。 也就是说,foo()的工作就是处理vector。
但在foo()的第二个版本中,隐含地给了第二个工作:创buildvector。 foo()版本1和foo()版本2之间的语义不一样。
而不是这样做,我会考虑只有一个foo()函数来处理一个向量,另一个函数创build向量,如果你需要这样的事情。
例如:
void foo(int i, const std::vector<int>& optional) { // process vector } std::vector<int>* makeVector() { return new std::vector<int>; }
显然,这些函数是微不足道的,如果所有makeVector()需要完成它的工作完全是刚刚调用new,那么makeVector()函数可能没有意义。 但是我相信,在你的实际情况下,这些函数比这里所显示的要多得多,上面的代码说明了一个基本的语义devise方法: 给一个函数做一个工作 。
上面的foo()函数的devise也说明了另外一个基本的方法,我个人在代码中使用它来devise接口 – 包括函数签名,类等。这就是:我相信一个好的接口1)使用方便直观,2)使用不当或不可能使用不当 。 在foo()函数的情况下,我们隐含地说,在我的devise中,vector需要已经存在并且“准备就绪”。 通过devisefoo()来获取引用而不是指针,调用者必须已经拥有一个向量,并且他们将很难通过一些不是一个随时可用的向量。
虽然我理解许多人对默认参数和过载的抱怨,但似乎对这些function提供的好处缺乏了解。
默认参数值:
首先我要指出的是,一个项目的初步devise,如果devise得当,应该没有什么用处。 但是,在违约最大的资产发挥作用的地方是现有的项目和成熟的API。 我工作的项目包含了数百万条现有的代码,并没有奢侈的重新编码所有。 所以当你想添加一个需要额外参数的新function时, 新参数需要缺省值。 否则,你会打破使用你的项目的每个人。 这对我个人来说可以,但是我怀疑你的公司或你的产品/ API的用户会喜欢在每次更新时重新编写他们的项目。 简而言之,默认值对于向后兼容性来说非常好! 这通常是您将在大API或现有项目中看到默认值的原因。
函数覆盖:函数覆盖的好处是它允许共享一个function概念,但是具有不同的选项/参数。 然而,很多时候,我看到函数replace懒惰地提供了截然不同的function,只是参数略有不同。 在这种情况下,它们应该分别具有独立命名的function,与它们的具体function有关(就像OP的例子一样)。
这些,C / C + +的特点是好的,使用得当的时候工作得很好。 可以说大部分的编程function都可以说。 这是他们滥用/滥用,他们造成问题。
免责声明:
我知道这个问题已经有几年了,但是由于这些答案出现在我今天(2012年)的search结果中,所以我觉得这需要未来的读者进一步解决。
我同意,我会使用两个function。 基本上,你有两个不同的用例,所以有两个不同的实现是有意义的。
我发现我编写的C ++代码越多,我拥有的参数默认值越less – 如果该function已被弃用,我也不会stream下任何眼泪,尽pipe我不得不重新编写旧代码的stream式加载。
在C ++中引用不能为NULL,真正好的解决scheme是使用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()) { } }
我通常避免第一种情况。 请注意,这两个function在它们的function上是不同的。 其中一个用一些数据填充vector。 另一个不(只接受来自调用者的数据)。 我倾向于命名不同的function,实际上做不同的事情。 事实上,即使你写这些,也是两个function:
-
foo_default
(或者只是foo
) -
foo_with_values
至less我觉得这个区别是很长的,对于偶尔的图书馆/function用户来说,这个区别更清洁。
我也喜欢第二个。 虽然两者之间没有太大的区别,但基本上在foo(int i)
过载中主要使用主要方法的function,主要的重载将完美地工作,而不关心另一个缺less存在的存在,所以还有更多在超载版本中分离问题。
在C ++中,您应该尽可能避免允许有效的NULL参数。 原因是它大大减less了现场文件。 我知道这听起来很极端,但我使用了10-20个参数的API,其中一半可以有效地为NULL。 由此产生的代码几乎是不可读的
SomeFunction(NULL, pName, NULL, pDestination);
如果你把它切换到强制const引用,代码只是被迫更可读。
SomeFunction( Location::Hidden(), pName, SomeOtherValue::Empty(), pDestination);
我正处于“超载”阵营。 其他人已经添加了关于您的实际代码示例的细节,但是我想添加我认为在一般情况下使用重载与默认值的好处。
- 任何参数都可以“默认”
- 如果重写函数的默认值使用不同的值,则不会有问题。
- 没有必要为现有types添加“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' }
没有必要为现有的types添加“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将解决这个问题)。
我赞成第三种select:分成两个function,但不要超载。
过载本质上不太可用。 他们要求用户了解两个选项,弄清楚它们之间的区别是什么,如果他们如此倾向,还要检查文档或代码,以确保哪个是哪个。
我会有一个函数接受参数,一个叫做“createVectorAndFoo”或类似的东西(显然命名变得更容易真正的问题)。
虽然这违反了“职能的两个职责”规则(并给它一个长名字),但我相信当你的函数确实做了两件事(创buildvector和foo)时,这是可取的。
一般来说,我同意他人使用双function方法的build议。 但是,如果在使用单参数表单时创build的vector总是相同,则可以通过改为静态并使用默认的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,或者是否是故意的,如果这是一个意外,那么你可能会造成一个错误。
随着第二个你可以testing(assert,无论)为NULL,并适当地处理它。