我有一个处理给定向量的函数,但如果没有给出,也可以自己创建这样的向量。
对于这种情况,我看到两种设计选择,其中函数参数是可选的:
将其设为指针并默认设为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
只是一个黑客?)。并且间接函数调用产生的性能差异可能会被优化掉。
然而,我经常在代码中看到第一个解决方案。除了程序员的懒惰之外,是否有令人信服的理由更喜欢它?
答案 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)
我正处于“超载”阵营。其他人已经添加了有关您的实际代码示例的细节,但我想添加我认为使用重载与一般情况下的默认值的好处。
在每个代码上加上一些代码示例:
任何参数都可以默认:
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并适当地处理它。