c++ 默认参数与重载?

chhkpiq4  于 2022-11-19  发布在  其他
关注(0)|答案(3)|浏览(155)

WG21 N0131中,比尔·吉本斯指出:
缺省参数通常被认为是过时的,因为它们可以被重载函数替换
我理解,单个函数如:

void f(T t = t0, U u = u0);

可以由三个重载代替:

void f() { f(t0); }
void f(T t) { f(t, u0); }
void f(T t, U u);

但我不明白的是,为什么后者要优于前者?(这就是他所说的“时代错误”,对吗?)
Google风格指南中有一些相关的讨论:Google C++ Styleguide〉默认参数,但我看不出它如何回答这个问题或支持Gibbons的声明。
有人知道他在说什么吗?为什么违约论被认为是一种混乱?

0s0u357o

0s0u357o1#

根据我自己的经验,问题在于与其他语言特性交互时违反了最小惊讶原则。假设您有一个经常使用f的组件。也就是说,您在很多地方都看到了这一点:

f();

通过阅读它,你可以假设你有一个不带参数的函数,所以当你需要添加与其他一些具有注册函数的组件的交互时:

void register(void (*cb)());

你做了显而易见的事。

register(f);

...然后你马上就会得到一个漂亮的错误,因为f的声明类型是一个带两个参数的函数。Wtf!?所以你看了声明就明白了...对吧...
默认参数通过编译器“fudging”调用位置使代码以某种方式运行。它并不是真正调用一个没有参数的函数,而是隐式初始化两个参数来调用函数。
另一方面,重载集的行为确实如人们所期望的那样。编译器没有“捏造”调用位置,当我们尝试register(f)时...它工作了!

aiazj4mn

aiazj4mn2#

选择重载的一个客观原因

最近有人向我提出了一个客观的(我认为是这样的!:D)理由,说明为什么人们应该更喜欢重载而不是默认参数,至少在默认值是非内置类型时是这样:头文件中不必要的#include指令。
默认参数应该是一个实现细节,因为 * 你 * 作为实现者代表你的客户决定使用什么参数,如果他们没有提供给你。为什么他们应该知道你的决定?所以当你有一个这样声明的函数

void doSomeWork(Foo, Bar = defaultBar);

您真的希望X1 M1 N1 X是一个“秘密”,而不是暴露给您的包含者。
现在你更喜欢默认参数,你必须把它们包含在你的头文件中,所有你需要的头文件都可以写defaultBar。这对你来说需要多少钱?
Bar可以是基类(引用或指针),而defaultBar是具体类的对象,因此必须同时包含定义了一个类和另一个类的头。
或者,Bar可能是std::function<bool(Foo const&, Foo const&)>,而它的默认值实际上是一个表达式,如[compose](https://www.boost.org/doc/libs/1_71_0/libs/hana/doc/html/compose_8hpp.html) (std::less<>{}, convertToInt),则在标题中会有以下内容:

// ok with these
//#include "Foo.hpp" // in the code below you don't even need the definiton of
                     // Foo, so you could be happy with just it's forward header
#include "fwd/Foo.hpp" // this only declares Foo
#include <functional>
// but why these?
#include <boost/hana/functional/compose.hpp> // or alternative
#include "/path/to/convertToInt.hpp" // maybe this does bring with it Foo.hpp

using Bar = std::function<bool(Foo const&, Foo const&)>;

void doSomeWork(Foo const&, Bar = compose(std::less<>{}, convertToInt));

对于重载,标头将为

// ok with these
#include "fwd/Foo.hpp" // this only declares Foo
#include <functional>

using Bar = std::function<bool(Foo const&, Foo const&)>;

void doSomeWork(Foo const&, Bar);
void doSomeWork(Foo const&);

并且只有在实现中,您才会包含其他头文件

#include "fwd/Foo.hpp"
#include "Foo.hpp"
#include <functional>
#include <boost/hana/functional/compose.hpp>
#include "/path/to/convertToInt.hpp"

void doSomeWork(Foo const& foo, Bar bar) {
   // definition
}

void doSomeWork(Foo const& foo) {
    doSomeWork(foo, compose(std::less<>{}, convertToInt));
}

原始答案

首先,我想参考this article on FluentC++,它解决了这个问题,并在文章顶部附近给出了一个明确的个人答案:

默认情况下,我认为我们应该首选默认参数而不是重载。

然而,正如 By default 所暗示的,作者在某些特殊情况下支持重载而支持缺省参数。
我原来的回答如下,但我不得不说:上面链接的文章确实大大减少了我对违约论点的反感。
给定void f(T t = t0, U u = u0);,您无法使用自定义的u调用f,并让t成为默认的t0(显然,除非您手动调用f(t0, some_u))。
有了重载,就很容易了:只需将f(U u)添加到重载集。
因此,使用重载可以做默认参数可以做的事情,还可以做更多的事情。
此外,既然这个问题我们已经陷入了争论,为什么不提一下这样一个事实,即你可以通过添加更多的默认值来重新声明函数呢?(例子取自cppreference。)

void f(int, int);     // #1 
void f(int, int = 7); // #2 OK: adds a default 
void f(int = 1, int); // #3 OK, adds a default to #2

如果函数的先前声明定义了默认参数,那么函数的定义就不能重新定义默认参数(for a pretty clear and understandable reason)。

void f(int, int = 7); // in a header file
void f(int, int) {} // in a cpp file correct
void f(int, int = 7) {} // in a cpp file wrong

是的,也许默认参数是一个“接口的东西”,所以可能在实现文件中看不到它的迹象是好的。

nwlls2ji

nwlls2ji3#

时代错误意味着某些东西突出地存在于现在,因为它被广泛认为是过去的事情。
我的回答的其余部分是一个观点的问题......但问题本身假设没有一个硬性的“答案”。
至于为什么默认参数已经成为过去,可能有很多例子......但是,我想到的最好的一个例子是,特别是在编写一组可重用函数时,我们希望减少错误/不正确使用的可能性。
请考虑以下事项:

void f(int i = 0, char c = 'A'){std::cout << i << c << std::endl;}

现在考虑有人试图使用它如下:

f('B');

他们可能希望看到以下输出:

0B

然而他们得到的却是:

66A

在看到输出后,他们理解了自己的错误并进行了纠正......但是如果您删除默认参数,并强制使用两个特定重载中的一个,这些重载将容纳两种类型的单个参数......那么您已经创建了一个更健壮的接口,它每次都提供预期的输出。默认参数可以工作......但它们不一定是最“清晰”的。在开发的情况下,当有人忘记如果在函数调用中提供了至少一个参数,则只有尾部参数可以被默认。
最后,重要的是代码是否有效......但是如果您看到带有标签和goto语句的代码,您可能会想:“哦,真的吗?"。它们工作得很好...但它们可能会被误用。切换语言来强调一般讨论的主观性质...如果JavaScript工作得很好,并提供了这么多的自由,考虑到其变量具有可变类型的性质...到底为什么会有人想要使用TypeScript?2这是一个简化/强制正确重用代码的问题。3否则谁在乎只要它能工作呢?

相关问题