2

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

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

#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

可能的后果:

哪个更具可读性?

哪个会提供更好的性能(或者它们会因为复制传播而等效)?直觉上,感觉从参数中读取会更有效率,因为它永远不会被写入,因此会产生更简单的依赖图

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

4

4 回答 4

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);
    }
}

这个函数绝对没有办法保证,this并且child是不同的指针。所以它不能value_在循环迭代之间保存在一个寄存器中,因为对的赋值child->value_可能已经改变this->value_。在这种情况下,即使是优秀的编译器也会希望看到您使用该参数。

可读性方面,如果您认为在您的成员名称之前或之后的下划线(或者m_,就此而言)使其不可读,那么您为什么要使用该符号?构造函数体和普通函数体之间的一致性绝对是可取的。所以我会争辩说,如果你的语义鼓励在函数执行期间将成员变量拉入局部变量,那么也在构造函数中执行此操作(只需使用参数)。但是如果在你的其他成员函数中没有使用这样的约定,那么也不要在构造函数中使用它——让编译器来处理它。

于 2012-06-27T04:35:13.743 回答
2

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

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

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

我无法想象任何性能原因会使一个与另一个显着不同,因此只有当分析器告诉我更改它会导致显着改进时,我才会选择不同的方法。

于 2012-06-27T04:59:08.097 回答
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];
            }
        }
    }
于 2012-06-27T04:07:19.103 回答
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 中的 5 或类似的规则)。但是,有几点建议:

  1. 当您_size = size;在构造函数的主体中执行此操作时,您不再进行初始化,而是进行赋值,这就是您应该使用初始化列表的原因(当然对于内置类型,这实际上是同一件事,但是,我会认为意图不同)。

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

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

另外,我不知道依赖图与这个问题有什么关系。

(个人注意:我从来不喜欢_成员变量前面的样式)

于 2012-06-27T04:07:36.543 回答