可选函数参数:使用默认参数(NULL)还是重载函数?

发布于 2024-07-15 13:14:05 字数 781 浏览 6 评论 0原文

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

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

将其设置为指针并默认设置为 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 吗?)。 并且间接函数调用导致的性能差异可能已被优化掉。

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

I have a function that processes a given vector, but may also create such a vector itself if it is not given.

I see two design choices for such a case, where a function parameter is optional:

Make it a pointer and make it NULL by default:

void foo(int i, std::vector<int>* optional = NULL) {
  if(optional == NULL){
    optional = new std::vector<int>();
    // fill vector with data
  }
  // process vector
}

Or have two functions with an overloaded name, one of which leaves out the argument:

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
}

Are there reasons to prefer one solution over the other?

I slightly prefer the second one because I can make the vector a const reference, since it is, when provided, only read, not written. Also, the interface looks cleaner (isn't NULL just a hack?). And the performance difference resulting from the indirect function call is probably optimized away.

Yet, I often see the first solution in code. Are there compelling reasons to prefer it, apart from programmer laziness?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(12

死开点丶别碍眼 2024-07-22 13:14:05

我不会使用这两种方法。

在这种情况下, 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() 设计为采用引用而不是指针,调用者必须已经拥有一个向量,这一点很直观,而且他们将很难传递不是现成向量的内容。

I would not use either approach.

In this context, the purpose of foo() seems to be to process a vector. That is, foo()'s job is to process the vector.

But in the second version of foo(), it is implicitly given a second job: to create the vector. The semantics between foo() version 1 and foo() version 2 are not the same.

Instead of doing this, I would consider having just one foo() function to process a vector, and another function which creates the vector, if you need such a thing.

For example:

void foo(int i, const std::vector<int>& optional) {
  // process vector
}

std::vector<int>* makeVector() {
   return new std::vector<int>;
}

Obviously these functions are trivial, and if all makeVector() needs to do to get it's job done is literally just call new, then there may be no point in having the makeVector() function. But I'm sure that in your actual situation these functions do much more than what is being shown here, and my code above illustrates a fundamental approach to semantic design: give one function one job to do.

The design I have above for the foo() function also illustrates another fundamental approach that I personally use in my code when it comes to designing interfaces -- which includes function signatures, classes, etc. That is this: I believe that a good interface is 1) easy and intuitive to use correctly, and 2) difficult or impossible to use incorrectly . In the case of the foo() function we are implictly saying that, with my design, the vector is required to already exist and be 'ready'. By designing foo() to take a reference instead of a pointer, it is both intuitive that the caller must already have a vector, and they are going to have a hard time passing in something that isn't a ready-to-go vector.

情深如许 2024-07-22 13:14:05

我肯定会赞成重载方法的第二种方法。

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

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

  • 如果需要将代码复制到两个方法中,则可以将其提取到单独的方法中,并且每个重载方法都可以调用此外部方法。
  • 我会更进一步,以不同的方式命名每个方法以指示方法之间的差异。 这将使代码更加自文档化。

I would definitely favour the 2nd approach of overloaded methods.

The first approach (optional parameters) blurs the definition of the method as it no longer has a single well-defined purpose. This in turn increases the complexity of the code, making it more difficult for someone not familiar with it to understand it.

With the second approach (overloaded methods), each method has a clear purpose. Each method is well-structured and cohesive. Some additional notes:

  • If there's code which needs to be duplicated into both methods, this can be extracted out into a separate method and each overloaded method could call this external method.
  • I would go a step further and name each method differently to indicate the differences between the methods. This will make the code more self-documenting.
捎一片雪花 2024-07-22 13:14:05

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

默认参数值:
首先我想指出,在项目的初始设计中,如果设计得当,默认值应该几乎没有用处。 然而,默认最大的资产发挥作用的是现有项目和完善的 API。 我所从事的项目由数百万行现有代码组成,但没有能力重新编码它们。 因此,当您希望添加需要额外参数的新功能时; 新参数需要默认值。 否则你会破坏所有使用你的项目的人。 这对我个人来说没问题,但我怀疑您的公司或您的产品/API 的用户是否会愿意在每次更新时重新编码他们的项目。 简单地说,默认值非常有利于向后兼容!这通常是您在大型 API 或现有项目中看到默认值的原因。

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

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

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

While I do understand the complaints of many people regarding default parameters and overloads, there seems to be a lack of understanding to the benefits that these features provide.

Default Parameter Values:
First I want to point out that upon initial design of a project, there should be little to no use for defaults if well designed. However, where defaults' greatest assets comes into play is with existing projects and well established APIs. I work on projects that consist of millions of existing lines of code and do not have the luxury to re-code them all. So when you wish to add a new feature which requires an extra parameter; a default is needed for the new parameter. Otherwise you will break everyone that uses your project. Which would be fine with me personally, but I doubt your company or users of your product/API would appreciate having to re-code their projects on every update. Simply, Defaults are great for backwards compatibility! This is usually the reason you will see defaults in big APIs or existing projects.

Function Overrides:
The benefit of function overrides is that they allow for the sharing of a functionality concept, but with with different options/parameters. However, many times I see function overrides lazily used to provide starkly different functionality, with just slightly different parameters. In this case they should each have separately named functions, pertaining to their specific functionality (As with the OP's example).

These, features of c/c++ are good and work well when used properly. Which can be said of most any programming feature. It is when they are abused/misused that they cause problems.

Disclaimer:
I know that this question is a few years old, but since these answers came up in my search results today (2012), I felt this needed further addressing for future readers.

缺⑴份安定 2024-07-22 13:14:05

我同意,我会使用两个函数。 基本上,您有两个不同的用例,因此有两种不同的实现是有意义的。

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

I agree, I would use two functions. Basically, you have two different use cases, so it makes sense to have two different implementations.

I find that the more C++ code I write, the fewer parameter defaults I have - I wouldn't really shed any tears if the feature was deprecated, though I would have to re-write a shed load of old code!

橘寄 2024-07-22 13:14:05

在 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()) {

   }
}

A references can't be NULL in C++, a really good solution would be to use Nullable template.
This would let you do things is ref.isNull()

Here you can use this:

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;
};

Now you can have

void foo(int i, Nullable<AnyClass> &optional = Nullable<AnyClass>()) {
   //you can do 
   if(optional.isNull()) {

   }
}
迷雾森÷林ヴ 2024-07-22 13:14:05

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

  • foo_default (或只是 foo
  • foo_with_values

至少我发现这种区别更清晰长期使用,适合偶尔使用库/函数的用户。

I usually avoid the first case. Note that those two functions are different in what they do. One of them fills a vector with some data. The other doesn't (just accept the data from the caller). I tend to name differently functions that actually do different things. In fact, even as you write them, they are two functions:

  • foo_default (or just foo)
  • foo_with_values

At least I find this distinction cleaner in the long therm, and for the occasional library/functions user.

念﹏祤嫣 2024-07-22 13:14:05

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

I, too, prefer the second one. While there are not much difference between the two, you are basically using the functionality of the primary method in the foo(int i) overload and the primary overload would work perfectly without caring about existence of lack of the other one, so there is more separation of concerns in the overload version.

十年不长 2024-07-22 13:14:05

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

SomeFunction(NULL, pName, NULL, pDestination);

如果您将其切换为强制 const 引用,则代码将变得更具可读性。

SomeFunction(
  Location::Hidden(),
  pName,
  SomeOtherValue::Empty(),
  pDestination);

In C++ you should avoid allowing valid NULL parameters whenever possible. The reason is that it substantially reduces callsite documentation. I know this sounds extreme but I work with APIs that take upwards of 10-20 parameters, half of which can validly be NULL. The resulting code is almost unreadable

SomeFunction(NULL, pName, NULL, pDestination);

If you were to switch it to force const references the code is simply forced to be more readable.

SomeFunction(
  Location::Hidden(),
  pName,
  SomeOtherValue::Empty(),
  pDestination);
╭ゆ眷念 2024-07-22 13:14:05

我完全属于“超载”阵营。 其他人添加了有关实际代码示例的细节,但我想添加我认为在一般情况下使用重载与默认值相比的好处。

  • 任何参数都可以是“默认值”,
  • 如果重写函数使用不同的默认值,则不会出现问题。
  • 没有必要向现有类型添加“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 会解决这个问题)。

I'm squarely in the "overload" camp. Others have added specifics about your actual code example but I wanted to add what I feel are the benefits of using overloads versus defaults for the general case.

  • Any parameter can be "defaulted"
  • No gotcha if an overriding function uses a different value for its default.
  • It's not necessary to add "hacky" constructors to existing types in order to allow them to have default.
  • Output parameters can be defaulted without needing to use pointers or hacky global objects.

To put some code examples on each:

Any parameter can be defaulted:

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
}

No danger of overriding functions having different values for the default:

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'
}

It's not necessary to add "hacky" constructors to existing types in order to allow them to be defaulted:

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 parameters can be defaulted without needing to use pointers or hacky global objects:

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);
}

The only exception to the rule re overloading versus defaults is for constructors where it's currently not possible for a constructor to forward to another. (I believe C++ 0x will solve that though).

瑶笙 2024-07-22 13:14:05

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

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

我会有一个接受参数的函数,
还有一个名为“createVectorAndFoo”或类似的东西(显然,对于实际问题,命名变得更容易)。

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

I would favour a third option:
Separate into two functions, but do not overload.

Overloads, by nature, are less usable. They require the user to become aware of two options and figure out what the difference between them is, and if they're so inclined, to also check the documentation or the code to ensure which is which.

I would have one function that takes the parameter,
and one that is called "createVectorAndFoo" or something like that (obviously naming becomes easier with real problems).

While this violates the "two responsibilities for function" rule (and gives it a long name), I believe this is preferable when your function really does do two things (create vector and foo it).

嗳卜坏 2024-07-22 13:14:05

一般来说,我同意其他人使用双功能方法的建议。 但是,如果使用 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) {
    ...
}

Generally I agree with others' suggestion to use a two-function approach. However, if the vector created when the 1-parameter form is used is always the same, you could simplify things by instead making it static and using a default const& parameter instead:

// 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) {
    ...
}
百思不得你姐 2024-07-22 13:14:05

第一种方法比较差,因为你无法判断你是否意外传入了 NULL 或者是否是故意的......如果这是一个意外,那么你很可能导致了错误。

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

The first way is poorer because you cannot tell if you accidentally passed in NULL or if it was done on purpose... if it was an accident then you have likely caused a bug.

With the second one you can test (assert, whatever) for NULL and handle it appropriately.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文