【问题标题】:Why should default parameters be added last in C++ functions?为什么要在 C++ 函数中最后添加默认参数?
【发布时间】:2010-11-10 13:23:25
【问题描述】:

为什么要在 C++ 函数中最后添加默认参数?

【问题讨论】:

  • 这不是没用的。应鼓励 IMO 了解语言设计(尤其是您使用的语言的设计)。要做到这一点,除其他事项外,您需要找出为什么您的“明显、简单”改进的想法是不切实际的,如果它们不切实际的话。以及为什么该语言的设计者错过了一个机会,如果他们不是不切实际的话。我很确定语言设计与编程有关。

标签: c++ function default calling-convention


【解决方案1】:

景曾是对的。我想在这里补充一下我的意见。调用函数时,参数从右到左压入堆栈。例如,假设您有这个任意函数。

int add(int a, int b) {
  int c;
  c = a + b;
  return c;
}

这是函数的堆栈帧:

------
  b
------
  a
------
 ret
------
  c
------

上图就是这个函数的栈帧!如您所见,首先将 b 压入堆栈,然后将 a 压入堆栈。之后,函数返回地址被压入堆栈。函数返回地址保存 main() 中最初调用函数的位置,在函数执行完成后,程序的执行将转到该函数的返回地址。然后将 c 等任何局部变量压入堆栈。

现在关键是参数从右到左被压入堆栈。基本上,提供的任何默认参数都是文字值,它们存储在可执行文件的代码部分中。当程序执行遇到一个没有相应参数的默认参数时,它会将这个字面值压入堆栈顶部。然后它查看 a 并将参数的值压入栈顶。堆栈指针始终指向堆栈顶部,即您最近推送的变量。因此,您作为默认参数压入堆栈的任何文字值都位于堆栈指针“后面”。

编译器首先快速将任意默认文字值首先推送到堆栈上可能更有效,因为它们没有存储在内存位置中,并且快速构建堆栈。想想如果变量先被压入堆栈,然后是字面量,会发生什么。与从电路或 CPU 寄存器中提取文字值相比,访问 CPU 的内存位置需要相对较长的时间。由于将变量压入堆栈与文字相比需要更多时间,因此文字必须等待,然后返回地址必须等待,局部变量也必须等待。这可能不是效率的大问题,但这只是我的理论,为什么默认参数总是在 C++ 中函数头的最右边位置。这意味着编译器就是这样设计的。

【讨论】:

    【解决方案2】:

    为了简化语言定义并保持代码可读性。

    void foo(int x = 2, int y);
    

    要调用它并利用默认值,您需要如下语法:

    foo(, 3);
    

    这可能让人觉得太奇怪了。另一种选择是在参数列表中指定名称:

    foo(y : 3);
    

    必须使用一个新符号,因为这已经意味着什么:

    foo(y = 3); // assign 3 to y and then pass y to foo.
    

    ISO 委员会考虑并拒绝了这种命名方法,因为他们不愿意在函数定义之外为参数名称引入新的意义。

    如果您对更多 C++ 设计原理感兴趣,请阅读 Stroustrup 的 The Design and Evolution of C++

    【讨论】:

    • 为什么我们不能使用 foo(3) 而不是 foo(,3)?是否有任何问题阻止编译器理解程序员的意图?我认为更好的理由是减少编译时出现歧义的机会,并使编译器的构建更容易。
    • 在 D & E 中,Stroustrup 说,由于函数声明和函数定义的分离,命名参数习语在 C++ 中很棘手。如果用户在声明中使用一个名称而在实现中使用另一个名称,编译器将不得不忽略其中一个。
    • Abhay - 我不认为他这么说 - 我认为他报告委员会中的一些人这么认为并给出了他们的理由。据我们所知,Stroustrup 本人会很好地依赖声明中的名称并忽略定义(我会这样做)。
    • @asklee,如果你的两个 par 都有默认值,并且你传入 foo(3) 怎么办?
    • @BorisCallens 那么显然 3 绑定到 y。就像现在一样,只是回到前面。
    【解决方案3】:

    这是关于调用约定的问题。 调用约定: 当你调用一个函数时,参数从右到左被压入堆栈。 例如

    fun(int a, int b, int c);
    

    堆栈是这样的: 一种 b C 所以,如果你像这样从左到右设置默认值:

    fun(int a = 1, int b = 2, int c);
    

    并像这样调用:

    fun(4,5);
    

    您的调用意味着设置 a = 4、b = 5 和 c = 无值; // 这是错误的!

    如果你这样声明函数:

    fun(int a, int b = 2, int c = 3);

    并像这样调用: fun(4, 5);

    你的调用意味着设置 a = 4, b = 5, and c = default value(3); // 这是对的!

    总之,你应该把默认值从右到左。

    【讨论】:

      【解决方案4】:

      标准委员会必须考虑的另一件事是默认参数如何与其他功能交互,例如重载函数、模板解析和名称查找。这些功能已经以非常复杂且难以描述的方式相互作用。让默认参数可以出现在任何地方只会增加复杂性。

      【讨论】:

        【解决方案5】:

        正如大多数答案所指出的那样,在参数列表中的任何位置都可能存在默认参数,这会增加函数调用的复杂性和模糊性(对于编译器而言,可能更重要的是对于函数的用户而言)。

        关于 C++ 的一个好处是,通常有一种方法可以做你想做的事(即使它并不总是一个好主意)。如果您想为各种参数位置设置默认参数,您几乎可以肯定地通过编写重载来实现这一点,这些重载只是转身并内联调用完全参数化的函数:

         int foo( int x, int y);
         int foo( int y) {
             return foo( 0, y);
         }
        

        你有相当于:

         int foo( int x = 0, int y);
        

        【讨论】:

          【解决方案6】:

          这是因为它使用参数的相对位置来查找它们对应的参数。

          它可以使用类型来识别未给出可选参数。但是隐式转换可能会干扰它。另一个问题是可能被解释为可选参数丢失而不是缺少参数错误的编程错误。

          为了让任何参数成为可选参数,应该有一种方法来识别参数以确保没有编程错误或消除歧义。这在某些语言中是可能的,但在 C++ 中是不可能的。

          【讨论】:

            【解决方案7】:

            作为一般规则,函数参数由编译器处理并按从右到左的顺序放置在堆栈中。因此,任何具有默认值的参数都应首先评估是有道理的。

            (这适用于 __cdecl,它往往是 VC++ 和 __stdcall 函数声明的默认值)。

            【讨论】:

            • 语言标准根本没有规定,所以这不是语言如何工作的原因 - 这是两个完全不同的关注级别。
            • 我承认我没有意识到这一点,但我知道这是 VC++ 和 gcc 编译器的行为。
            【解决方案8】:

            想象一下你有一个带有这个原型的函数:

            void testFunction(bool a = false, bool b = true, bool c);
            

            现在假设我这样调用函数:

            testFunction(true, false);
            

            编译器应该如何确定我打算为哪些参数提供值?

            【讨论】:

            • 它会解决的,因为会有规则。例如,参数将绑定到第二个和第三个参数。
            【解决方案9】:

            如果定义如下函数:

            void foo( int a, int b = 0, int c );
            

            如何调用函数并为 a 和 c 提供值,但保留 b 作为默认值?

            foo( 10, ??, 5 );
            

            与其他一些语言(例如 Python)不同,C/C++ 中的函数参数不能通过名称来限定,如下所示:

            foo( a = 10, c = 5 );
            

            如果可能,那么默认参数可以在列表中的任何位置。

            【讨论】:

            • 下面这样的情况呢..foo(int a,double b=0.0,char c='a')
            • 其实,如果编译器足够聪明,我们应该可以写出 foo(10, 5)。编译器应将其解释为 foo(10, default, 5)。
            • @raj:这在 C++ 中是允许的。你可以把它想象成这样,一旦你开始给出默认参数,你必须一直给出到最后(即直到最后一个参数),而不能在两者之间留下任何东西。
            • @askalee:这并不简单。想象一下我在中间指定了两个默认参数的情况,我想覆盖其中一个默认参数。那么编译器应该如何猜测要覆盖哪一个。
            • @raj - 很高兴您提出问题,因为它可以让我们从中获得声誉,但另一方面,您是否考虑过在 C++ 编译器中尝试这些事情?这样你就可以很容易地立即找到答案。
            猜你喜欢
            • 2015-11-28
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2018-04-30
            相关资源
            最近更新 更多