默认参数

Minimal example

int foo(int x, int y=1) { return x + y; }
 
int main()
{
    cout << foo(5);  // call foo(5, 1)
    return 0;
}

分离编译带来的隐患

如果函数声明和定义分离,此时就有一个pitfall。由于默认参数可以定义在函数声明(declaration)中,也可以定义在函数定义(definition)中。

Default argument in function definition

// foo.h
int foo(int x, int y);
 
// foo.cpp
#include "foo.h"
int foo(int x, int y=1) { return x + y; }
 
// main.cpp
#include <iostream>
#include "foo.h"
 
int main()
{
    std::cout << foo(3);  // error
}

如果改写main.cpp:

#include <iostream>
int foo(int x, int y=2);
 
int main()
{
    std::cout << foo(3);
}

使用命令行编译

g++ foo.cpp main.cpp

运行之,猜一下结果?4 or 5?(答案5)

很违背直觉是吗?这就是默认参数所带的一系列副作用。所以,在实际工程中,除非特别简单的情况,否则不建议使用默认参数。

我目前没看过底层原理,只作个简单猜想。这其实是函数定义的不一致,但由于这两个文件是分离编译的,编译器无法处理两个文件中不一致的声明,因为编译器是一个文件一个文件处理的。处理一个丢一个。所以,上述代码可以通过编译,并能成功链接(foo.cpp中提供了foo的定义,main.cpp中提供了foo的前置声明,二者函数原型是一样的,因此也能链接上)。但最后输出的结果,在调用foo(5)是,编译器查找默认参数的时候,优先使用了本文件(main.cpp)函数声明中定义的默认参数,而非其他文件(foo.cpp)中函数定义中定义的默认参数。

如果继续改写main.cpp

#include <iostream>
int foo(int x, char y=2);
 
int main()
{
    std::cout << foo(3);
}

你会发现此时链接器就报错了,说找不到int foo(int, char)的定义。

$ g++ foo.cpp main.cpp
/usr/bin/ld: /tmp/ccdz0bAH.o: in function `main':
main.cpp:(.text+0xf): undefined reference to `foo(int, char)'
collect2: 错误:ld 返回 1

这也印证了,上一个例子确实是函数声明中对默认参数的定义不一致,但编译器无法察觉。

Default arguments in function declaration

// foo.h
int foo(int x, int y=1);
 
// foo.cpp
#include "foo.h"
int foo(int x, int y) { return x + y; }
 
// main.cpp
#include <iostream>
#include "foo.h"
 
int main()
{
    std::cout << foo(3);  // ok, call foo(3, 1)
}

此时在main.cpp中可以省略默认参数,因为编译main.cpp的时候,先将foo.h插入(预处理),此时当走到foo(3)的时候,编译器能拿到默认参数的定义,所以可以成功调用。

如果改写foo.cpp:

#incldue "foo.h"
int foo(int x, int y=2) { return x + y; }

此时使用g++可以编译成功,运行结果为4(使用的foo.h中函数声明中定义的默认参数)。

但使用clang时,无法通过编译,会报一个redefinition of default argument的错误:

$ clang++ main.cpp foo.cpp
foo.cpp:3:20: error: redefinition of default argument
int foo(int x, int y=2)
                   ^ ~
./foo.h:4:20: note: previous definition is here
int foo(int x, int y=1);
                   ^ ~
1 error generated.

需要说明的是,这是合理的结果,clang的做法无疑是更科学的。

https://godbolt.org/z/Wfcz7dsrE

与虚函数结合带来的隐患

struct Cat {
    virtual void speak(const char *s = "meow")
        { printf("%s.\n", s); }
};
struct Tiger : public Cat {
    void speak(const char *s = "roar") override
        { printf("%s!!\n", s); }
};
 
Cat c;       c.speak();  // "meow."
Tiger t;     t.speak();  // "roar!!"
Cat *p = &t; p->speak(); // "meow!!"

可以看到,当用基类指针调虚函数时,默认参数使用的是基类中提供的那个,是不是又违反直觉了?因为默认参数是编译时确定的,而动态绑定是运行时确定的。在编译完成时,默认参数已经根据静态类型(因为是基类指针,所以是基类类型)进行了填充,运行时发生动态绑定调用派生类的方法这没问题,但是参数传的是基类默认参数。

更多pitfall参见ref1,其中提到了默认参数的种种罪恶,并奉劝大家不要使用默认参数!

References

  1. https://quuxplusone.github.io/blog/2020/04/18/default-function-arguments-are-the-devil/
  2. https://www.geeksforgeeks.org/default-arguments-and-virtual-function-in-cpp/