【问题标题】:Optional function parameters: Use default arguments (NULL) or overload the function?可选函数参数:使用默认参数(NULL)还是重载函数?
【发布时间】: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 吗?)。并且间接函数调用导致的性能差异可能被优化掉了。

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

【问题讨论】:

    标签: c++ function parameters null overloading


    【解决方案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&amp; 参数来简化事情:

            // 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 并适当地处理它。

              【讨论】:

                猜你喜欢
                • 2015-06-19
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2021-05-14
                • 1970-01-01
                • 2015-05-08
                • 2011-05-07
                相关资源
                最近更新 更多