C ++样式:在构造函数体中使用参数或成员

时间:2012-06-27 03:42:57

标签: c++ coding-style constructor

这是我经常提出的一个问题,最后想听听人们对他们喜欢的风格的看法。

在构造函数中使用(仅用于只读目的)参数或成员是更好/首选的做法吗?例如,在这个简单的向量类中:

#include <iostream>
#include <array>

class SimpleDoubleVector {
private:
  double * _data;
  std::size_t _size;
public:
  SimpleDoubleVector(double * data, std::size_t size) :
  _size(size) {
    _data = new double[size];
    for (int k=0; k<size; ++k)
      _data[k] = data[k];
  }
  ~SimpleDoubleVector() {
    delete[] _data;
  }
};

是否更好?
  1. 在整个构造函数中使用size(如图所示)
  2. 首先分配/初始化_size,然后使用_size
  3. 可能的后果:

    哪个更具可读性?

    哪个会提供更好的效果(或者因为copy propagation而两者都相同)?直观地说,从参数读取会更有效率,因为它永远不会被写入,因此会导致更简单的dependency graph

    我知道这可能看起来很迂腐,但它经常出现,我真的想剖析最好的方法(或者至少更好地理解利弊是什么)。

4 个答案:

答案 0 :(得分:2)

在语义上,局部变量(以及参数)通常比成员变量更受欢迎。拿这个有点捏造的例子:

class Complex {
    float real_;
    float imag_;

public:
    Complex& operator*=(const Complex& that) {
        real_ = real_ * that.real_ - imag_ * that.imag_;
        imag_ = imag_ * that.real_ + real_ * that.imag_;
    }
};

乍看之下,直到您意识到第一行中real_的修改改变了第二行中real_的值。即使你抓住它并将原始real_存储在局部变量中,你也可能是c *= c,其中操作符的左侧和右侧是别名,并且您在第一行中real_的更改无意中在第二行中更改了that.real_。换句话说,对成员变量的更改能够导致局部变量更改的副作用。

速度,任何合理的编译器都会发现两者相同。如果重用参数,不合理的编译器可能会生成更好的代码,因为它已经在本地,并且编译器确实知道除了可以看到的代码之外什么都不能改变该值。同样值得注意的是,即使在优秀的编译器上,轻微复杂的情况也会对这样的情况产生更差的输出:

void MyClass::foo(int value, MyClass* child) {
    value_ = value;
    for (int i = 0; i < value_; ++i) {
        if (child) child->value_ = i;
        bar(i, child);
    }
}

此函数绝对无法保证thischild是不同的指针。因此,它不能将value_保留在循环迭代之间的寄存器中,因为child->value_的赋值可能已更改this->value_。在这种情况下,即使是好的编译器也希望看到你使用参数。

可读性方面如果您认为在您的会员名称之前或之后使用下划线(或m_,那么)使其不可读,那么您为什么要使用该表示法?构造函数体和正常函数体之间的一致性是绝对可取的。所以我认为如果你的语义鼓励在一个函数的持续时间内将成员变量拉入局部变量,那么在构造函数中也是如此(只需使用参数)。但是如果你的其他成员函数中没有使用这样的约定,那么不要在构造函数中使用它 - 让编译器处理它。

答案 1 :(得分:2)

我总是在构造函数中使用参数(如果可能),原因有两个:

1)我正在从外部输入初始化对象状态。使用这些参数强调了对外部数据的使用。

2)当更广泛地使用初始化列表时,它可以防止在初始化之前使用类成员的各种问题(由于初始化顺序由成员顺序指定,而不是构造函数中的初始化顺序)。

我无法想象任何可能使其与另一个产生明显不同的性能原因,所以只有当一个分析师告诉我改变它会导致显着的改进时,我才会选择不同的方法。

答案 2 :(得分:1)

如果问题是可读性,答案应该是初始化列表。由于首先列出_data,因此问题有点被迫。

    SimpleDoubleVector(double * data, std::size_t size)
        : _data(std::copy(data, data+size, new double[size])),
          _size(size)
        {}

如果首先列出_size,则可以选择,但在这种情况下我会选择使用参数,因为在没有_的情况下,源代码更容易阅读。我相信std::copy性能差异可以忽略不计。

如果参数和数据成员名称中的名称具有1-1对应关系,那么如果必须在构造函数的主体中进行初始化,我会使用相同的推理。如果使用某种参数计算初始化数据成员,那么如果它对其他数据成员的初始化有用,则代码应该使用计算值。如果存在复杂的初始化,则将该初始化放在单独的函数中通常很有用。这预示着多个构造函数。可以编写此函数以利用初始化的数据成员,以便最小化构造函数和初始化函数之间传递的参数。

    SimpleDoubleVector(double * data, std::size_t size) {
        _size = size;
        initialize_data(data);
    }

    SimpleDoubleVector(std::size_t size) {
        _size = size;
        initialize_data();
    }

    double * initialize_data(double * data = 0) {
        _data = new double[_size];
        if (data) {
            for (std::size_t k = 0; k < _size; ++k) {
                _data[k] = data[k];
            }
        }
    }

答案 3 :(得分:1)

我会按如下方式制作课程:

class SimpleDoubleVector {
private:
  std::size_t _size; // Make sure this is declared first!!
  double * _data;
public:
  SimpleDoubleVector(double * data, std::size_t size) : 
      _size(size), data(new double[size]) // Use initialization lists
  {
    for (int k = 0; k < _size; ++k) // Could eliminate all this with std::vector
      _data[k] = data[k];
  }
  ~SimpleDoubleVector() {
    delete[] _data;
  }
};

当然这不是所有代码,因为您正在管理资源,您需要实现三阶规则(或者类似于C ++ 11中的规则)。 但是,一些指示:

  1. 当您在构造函数的主体中执行_size = size;时,您不再进行初始化,而是进行赋值,这就是为什么您应该使用初始化列表(对于内置类型的课程,这是有效相同的事情,但是,我认为意图是不同的。)

  2. 传递给构造函数的参数是为了在示例中初始化成员变量。除了执行初始化之外,不应将它们用于任何其他目的。

  3. 使用std::vector<double>std::array<double>可能会更好,但我确信这与问题无关。

  4. 另外,我不知道依赖图如何与这个问题相关。

    (个人注意:我从未喜欢为成员变量添加_的样式)