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

C++ style: use parameter or member in constructor body

本文关键字:参数 成员 主体 样式 构造函数 C++      更新时间:2023-10-16

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

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

#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

可能的后果:

哪个更具可读性?

哪个会提供更好的性能(或者由于复制传播,它们都是等效的)?直观地说,感觉从参数读取会更有效率,因为它永远不会被写入,因此会产生更简单的依赖关系图。

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

从语义上讲,局部变量(以及参数)通常优先于成员变量。举这个有点捏造的例子:

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) 当更广泛地使用初始值设定项列表时,它可以防止在初始化之前使用类成员的各种问题(由于初始化顺序由成员顺序指定,而不是构造函数中的初始值设定项顺序)。

我想不出任何性能原因会使一个与另一个明显不同,所以只有当探查器告诉我改变它会导致显着改进时,我才会选择不同的方法。

我会按如下方式制作类:

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

当然,这不是全部代码,因为您正在管理资源,因此您需要实现三法则(或 5 或 C++11 中的类似规则)。但是,有几点提示:

  1. 当你在构造函数的主体中_size = size;时,你不再做初始化,你正在做赋值,这就是为什么你应该使用初始化列表(当然对于内置类型,这实际上是一回事,但是,我认为意图是不同的)。

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

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

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

(个人说明:我从来不喜欢成员变量的前缀_样式)

如果问题是可读性,答案应该是初始值设定项列表。由于_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];
            }
        }
    }