修改 std::vector 函数(继承?)

Modifying a std::vector function (inheritance?)

我正在将一些 Fortran90 代码移植到 C++(因为我很笨,为了节省 "Why?!")。

Fortran 允许指定数组的范围,特别是从负值开始,例如

double precision :: NameOfArray(FirstSize, -3:3)

我可以用 C++ 将其写成类似

std::array<std::array<double, 7>, FirstSize> NameOfArray;

但现在我必须像 NameOfArray[0:FirstSize-1][0:6] 一样索引。如果我想使用 Fortran 风格索引进行索引,我可以这样写 perhaps

template <typename T, size_t N, int start>
class customArray
{
public:
    T& operator[](const int idx) { return data_[idx+start]; }
private:
    std::array<T,N> data_;
}

然后

customArray<double, 7, -3> NameOfArray;
NameOfArray[-3] = 5.2;
NameOfArray[3] = 2.5;
NameOfArray[4] = 3.14; // This is out of bounds, 
                       // despite being a std::array of 7 elements

所以 - 总体思路是 "Don't inherit from std::'container class here'"。 我的理解是,这是因为,例如,std::vector 没有虚拟析构函数,因此不应该(不能?)被多态使用。

有没有其他方法可以使用 std::arraystd::vector 等,并获得它们的功能 'for free',同时覆盖特定功能?

template<typename T, size_t N>
T& std::array<T,N>::operator[](const int idx) { ... };

可能允许我覆盖运算符,但它不会让我访问有关自定义起点的知识 - 使其完全没有意义。此外,如果我乐观地认为我所有的 customArray 对象都具有相同的偏移量,我可以对该值进行硬编码 - 但是我的 std::array 被破坏了(我认为)。

我该如何解决这个问题? (忽略简单的答案 - 不要 - 根据需要写 myArray[idx-3]

继承标准容器没有问题。这只是 通常不鼓励 因为这强加了一些限制,并且这种继承不是 C++ 中最初预测的继承方式。如果您小心并意识到这些限制,您可以在这里安全地使用继承。

您只需要记住这个 不是 subclassing 以及它的真正含义。特别是,您不应使用指向此 class 对象的指针或引用。问题可能是,如果您在预期 vector<x>* 的地方传递了 MyVector<x>* 的值。您也不应该创建动态对象(使用 new),因此也不要通过指向基 class 的指针来创建这些对象 delete - 仅仅是因为析构函数调用不会转发给您的 class 的析构函数,因为它不是 virtual.

无法阻止将 "derived pointer" 强制转换为 "base pointer",但您可以通过重载 & 运算符来阻止从对象获取指针。您还可以通过在私有部分声明一个 in-class operator new 来阻止动态创建此 class 的对象(或者 = delete 也应该有效)。

私有继承也别想了。这就像将这个东西作为一个字段包含在私有部分中,除了访问者名称。

范围转换器 class 可能是解决方案,尽管您需要自己制作它,但它可以让您获得范围大小来初始化向量并进行转换。

未经测试的代码:

struct RangeConv // [start,end[
{
  int start, end;
  RangeConv(int s, int e) : start(s), end(e) { }
  int size() const { return end - start; }
  int operator()(int i) { return i - start; } // possibly check whether in range
}

RangeConv r(-3, 3);
std::vector<int> v(r.size());
v[r(-3)] = 5;

so should not (can not?) be used polymorphically.

不要太早放弃。在 C++ 中继承基本上有两个问题需要考虑。

终生

如果您基本上遵循一个简单的规则:不要在任何地方使用 delete,则可以以多态方式安全地以多态方式安全地使用此类派生对象 类 .这自然意味着你不能使用new。无论如何,您通常应该避免 new 和现代 C++ 中的原始指针。 shared_ptr 会做正确的事情,即安全地调用正确的析构函数,只要您使用 make_shared:

std:: shared_ptr<Base> bp = std:: make_shared<Derived>( /* constructor args */ );

make_shared 的类型参数,在本例中 Derived,不仅控制创建的类型。它还控制调用哪个析构函数。 (因为底层共享指针对象将存储适当的删除器。)

使用 unique_ptr 很诱人,但不幸的是(默认情况下)它会导致使用错误的删除器(即它会天真地直接在基指针上使用 delete)。不幸的是,除了默认 unique_ptr 之外,标准中并没有内置更安全但效率较低的 unique_ptr_with_nice_deleter

多态性

即使std::array有虚析构函数,现在的设计还是很奇怪的。因为 operator[] 不是虚拟的,所以从 customArray* 转换为 std:: array* 会导致错误的 operator[]。这实际上不是特定于 C++ 的问题,它基本上是您不应该假装 customArray isa std:: array.

的问题

相反,只需确定 customArray 是一个单独的类型。这意味着你不能将 customArray* 传递给期望 std::array* 的函数——但你确定你想要那个吗?

Is there some other way I can use a std::array, std::vector, etc, and get their functions 'for free', whilst overloading specific functions?

这是个好问题。您 希望您的新类型满足 isa std::array。您只是希望它的行为与它非常相似。就好像您神奇地复制并粘贴了 std::array 中的所有代码以创建一个新类型。然后你想调整一些东西。

使用private继承和using子句引入你想要的代码:

template <typename T, size_t N, int start>
struct customArray : private std::array<T,N>
{
    // first, some functions to 'copy-and-paste' as-is
    using std::array<T,N>  :: front;
    using std::array<T,N>  :: begin;

    // finally, the functions you wish to modify
    T& operator[](const int idx) { return data_[idx+start]; }
}

private 继承将阻止从 customArray *std::array * 的转换,这就是我们想要的。

PS: 我对这样的 private 继承经验很少。这么多它不是最好的解决方案 - 任何反馈表示赞赏。

总体思路

建议不要从标准 vector 继承,因为这种构造经常被误解,有些人试图让所有类型的对象都从 vector 继承,只是为了一点点方便。

但这条规则不应该成为教条。特别是如果您的目标是制作矢量 class,并且您知道自己在做什么。

危险一:不一致

如果您有一个非常重要的代码库使用 1..size 而不是 0..size-1 范围内的向量,您可以选择根据此逻辑保留它,以免添加数千个 - 1 到索引,+1 到索引显示,+1 到大小。

一个有效的方法可能是使用类似的东西:

template <class T> 
class vectorone : public vector<T> {
    public: 
    T& operator[] (typename vector<T>::size_type n) { return vector<T>::operator[] (n-1); }
const T& operator[] (typename vector<T>::size_type n) const { return  vector<T>::operator[] (n-1); }
};

但是你必须在所有矢量界面上保持一致:

  • 首先,还有一个const T& operator[]()。如果你不重载它,如果你在常量对象中有向量,你最终会出现错误的行为。
  • 然后,上面没有了,还有一个at(),它应该与[]
  • 一致
  • 然后你必须格外小心构造函数,因为它们有很多,以确保你的论点不会被误解。

因此您拥有免费功能,但前方的工作比最初想象的要多。使用更有限的接口和私有向量创建自己的对象的选择最终可能是一种更安全的方法。

危险2:more不一致

向量索引是vector<T>::size_type。不幸的是,这种类型是无符号的。 inherit from vector, but redefine operator[] with signed integer indexes 的影响必须仔细分析。根据索引的定义方式,这可能会导致细微的错误。

结论:

您认为可能需要做更多工作才能提供一致的 std::vector 界面。所以最后,拥有自己的 class 使用私有向量可能是更安全的方法。

您还应该考虑到有一天您的代码将由没有 Fortran 背景的人维护,并且他们可能对您代码中的 [] 有错误的假设。使用原生 C++ 真的没有问题吗?

坚持组合并为您需要的成员函数编写包装器似乎并没有那么糟糕。没有那么多。我什至想将 array 数据成员设置为 public 以便您可以在需要时直接访问它,尽管有些人会认为这比从基础 [=14= 继承更大的禁忌] 没有虚拟析构函数。

template <typename T, size_t N, int start>
class customArray
{
public:
    std::array<T,N> data;

    T& operator[](int idx) { return data[idx+start]; }
    auto begin() { return data.begin(); }
    auto begin() const { return data.begin(); }
    auto end() { return data.end(); }
    auto end() const { return data.end(); }
    auto size() const { return data.size(); }
};

int main() {
    customArray<int, 7, -3> a;
    a.data.fill(5);  // can go through the `data` member...
    for (int& i : a) // ...or the wrapper functions (begin/end).
        cout << i << endl;
}