带有C++构造函数的默认参数

Default parameters with C++ constructors

本文关键字:默认 参数 构造函数 C++ 带有      更新时间:2023-10-16

有一个使用默认参数的类构造函数是一个好做法,还是应该使用单独的重载构造函数?例如:

// Use this...
class foo  
{
private:
    std::string name_;
    unsigned int age_;
public:
    foo(const std::string& name = "", const unsigned int age = 0) :
        name_(name),
        age_(age)
    {
        ...
    }
};
// Or this?
class foo  
{
private:
    std::string name_;
    unsigned int age_;
public:
    foo() :
    name_(""),
    age_(0)
{
}
foo(const std::string& name, const unsigned int age) :
        name_(name),
        age_(age)
    {
        ...
    }
};

任何一个版本似乎都有效,例如:

foo f1;
foo f2("Name", 30);

你更喜欢或推荐哪种风格,为什么?

绝对是风格问题。我更喜欢带有默认参数的构造函数,只要这些参数有意义。标准中的类也使用它们,这对它们有利。

需要注意的一点是,如果除了一个参数外,其他参数都有默认值,则可以从该参数类型隐式转换类。查看此线程以了解更多信息。

我会使用默认参数,尤其是因为C++不允许您链接构造函数(因此您最终不得不为每个重载复制初始化程序列表,甚至可能更多)。

也就是说,有一些gotcha具有默认参数,包括常量可能是内联的(从而成为类的二进制接口的一部分)。另一个需要注意的是,添加默认参数可以将显式多参数构造函数转换为隐式单参数构造函数:

class Vehicle {
public:
  Vehicle(int wheels, std::string name = "Mini");
};
Vehicle x = 5;  // this compiles just fine... did you really want it to?

此讨论既适用于构造函数,也适用于方法和函数。

使用默认参数

好的是,您不需要为每种情况重载构造函数/方法/函数:

// Header
void doSomething(int i = 25) ;
// Source
void doSomething(int i)
{
   // Do something with i
}

糟糕的是,你必须在头中声明默认值,这样你就有了一个隐藏的依赖性:就像当你更改内联函数的代码时,如果你更改了头中的默认值,你需要使用这个头重新编译所有源,以确保它们将使用新的默认值。

如果不这样做,源仍将使用旧的默认值。

使用重载的构造函数/方法/函数

好的方面是,如果函数没有内联,那么可以通过选择一个函数的行为来控制源中的默认值。例如:

// Header
void doSomething() ;
void doSomething(int i) ;
// Source
void doSomething()
{
   doSomething(25) ;
}
void doSomething(int i)
{
   // Do something with i
}

问题是,您必须维护多个构造函数/方法/函数及其转发。

根据我的经验,默认参数当时看起来很酷,让我的懒惰因素很开心,但后来我使用了这个类,当默认值出现时,我很惊讶。所以我真的不认为这是个好主意;最好先有一个className::className(),然后是一个className::init(arglist)。只是为了获得可维护性优势。

Sam的回答给出了默认参数更适合构造函数而不是重载的原因。我只想补充一点,C++-0x将允许从一个构造函数委派到另一个构造函数,从而消除对默认值的需要。

两种方法都有效。但是,如果您有一长串可选参数,请创建一个默认构造函数,然后让set函数返回对此的引用。然后用链条锁住设置器。

class Thingy2
{
public:
    enum Color{red,gree,blue};
    Thingy2();
    Thingy2 & color(Color);
    Color color()const;
    Thingy2 & length(double);
    double length()const;
    Thingy2 & width(double);
    double width()const;
    Thingy2 & height(double);
    double height()const;
    Thingy2 & rotationX(double);
    double rotationX()const;
    Thingy2 & rotatationY(double);
    double rotatationY()const;
    Thingy2 & rotationZ(double);
    double rotationZ()const;
}
main()
{
    // gets default rotations
    Thingy2 * foo=new Thingy2().color(ret)
        .length(1).width(4).height(9)
    // gets default color and sizes
    Thingy2 * bar=new Thingy2()
        .rotationX(0.0).rotationY(PI),rotationZ(0.5*PI);
    // everything specified.
    Thingy2 * thing=new Thingy2().color(ret)
        .length(1).width(4).height(9)
        .rotationX(0.0).rotationY(PI),rotationZ(0.5*PI);
}

现在,在构造对象时,您可以拾取并选择要覆盖的特性以及已设置的特性是显式命名的。可读性更强:)

此外,您不再需要记住构造函数参数的顺序。

还需要考虑的一件事是该类是否可以在数组中使用:

foo bar[400];

在这种情况下,使用默认参数没有任何好处。

这肯定不会奏效:

foo bar("david", 34)[400]; // NOPE

主要是个人选择。但是,重载可以做默认参数可以做的任何事情,但不能反之亦然。

示例:

您可以使用重载写入A(int x,foo&A)和A(int x),但不能使用默认参数写入A(intx,foo&=null)。

一般规则是使用任何有意义的东西,使代码更可读。

如果用参数创建构造函数很糟糕(正如许多人所说),那么用默认参数创建构造函数就更糟糕了。我最近开始认为ctor参数不好,因为你的ctor逻辑应该尽可能地最小化。如果有人传入一个没有任何意义的参数,你如何处理ctor中的错误处理?你可以抛出一个异常,这是个坏消息,除非你的所有调用者都准备好将任何"新"调用封装在try块中,或者设置一些"已初始化"的成员变量,这是一种肮脏的黑客行为。

因此,确保传递到对象初始化阶段的参数的唯一方法是设置一个单独的initialize()方法,在该方法中可以检查返回代码。

默认参数的使用是不好的,原因有两个:;首先,如果您想向ctor添加另一个参数,那么您需要将其放在开头并更改整个API。此外,大多数程序员习惯于通过在实践中使用的方式来计算API——这是,尤其是对于在可能不存在正式文档的组织内部使用的非公共API来说。当其他程序员看到大多数调用都不包含任何参数时,他们也会这样做,并愉快地不知道默认参数强加给他们的默认行为。

此外,值得注意的是,谷歌C++风格指南将ctor参数(除非绝对必要)和默认参数都避开了函数或方法。

我会使用默认参数,因为这个原因:您的示例假设ctor参数直接对应于成员变量。但是,如果不是这样,并且必须在初始化对象之前处理参数,该怎么办。拥有一个共同的ctor将是最好的方法。

默认参数困扰我的一件事是,您不能指定最后一个参数,而是使用第一个参数的默认值。例如,在你的代码中,你不能创建一个没有名字但有给定年龄的Foo(然而,如果我没记错的话,这在C++0x中是可能的,使用统一的构造语法)。有时,这是有道理的,但也可能真的很尴尬。

在我看来,没有经验法则。就个人而言,我倾向于使用多个重载构造函数(或方法),除非只有最后一个参数需要默认值。

风格很重要,但正如Matt所说,一定要考虑用默认参数标记构造函数,这将允许隐式转换为"显式",以避免意外的自动转换。这不是一个要求(如果您正在制作一个要隐式转换为的包装器类,可能也不太好),但它可以防止错误。

我个人喜欢在适当的时候使用默认值,因为我不喜欢重复的代码。YMMV。