6

我正在做一些 C++ 计算力学(别担心,这里不需要物理知识),有些事情真的让我很困扰。

假设我想表示一个 3D 数学向量(与 std::vector 无关):

class Vector {
    public:
        Vector(double x=0., double y=0., double z=0.) { 
            coordinates[0] = x;
            coordinates[1] = y;
            coordinates[2] = z;
        }
    private:
        double coordinates[3];
};

到目前为止,一切都很好。现在我可以重载 operator[] 来提取坐标:

double& Vector::operator[](int i) {
     return coordinates[i] ;
}

所以我可以输入:

Vector V; 

… //complex computation with V

double x1 = V[0];
V[1] = coord2;

问题是,从 0 开始索引在这里并不自然。我的意思是,在对数组进行排序时,我不介意,但事实是,每篇论文、书籍或任何东西中的常规符号总是以 1 开头的坐标。这似乎是一个小问题,但事实是在公式中,它总是需要仔细考虑才能理解我们正在做什么。当然,这对于矩阵来说是最糟糕的。

一个明显的解决方案是稍微不同的重载:

double& Vector::operator[](int i) {
     return coordinates[i-1] ;
}

所以我可以输入

double x1 = V[1];
V[2] = coord2;

除了一件事之外,它似乎很完美:这个 i-1 减法似乎是一个小开销的好候选。你会说很小,但我正在做计算力学,所以这通常是我们负担不起的。

所以现在(最后)我的问题:你认为编译器可以优化这个,还是有办法让它优化?(模板、宏、指针或参考组合...)

逻辑上,在

double xi = V[i];

括号之间的整数大部分时间都是文字(除了在 3 次迭代 for 循环中),内联 operator[] 应该使它成为可能,对吧?

(对不起这个冗长的问题)

编辑:

感谢您的所有评论和回答

我有点不同意人们告诉我我们习惯于 0-indexed 向量。从面向对象的角度来看,我认为没有理由将数学向量设为 0 索引,因为它是使用 0 索引数组实现的。我们不应该关心底层实现。现在,假设我不关心性能并使用映射来实现 Vector 类。然后我会发现将'1'与'1st'坐标映射是很自然的。

也就是说,我尝试了 1-indexed 向量和矩阵,并且在编写了一些代码之后,我发现每次使用数组时它都不能很好地交互。我认为 Vector 和容器 (std::array,std::vector...) 不会经常交互(意思是在彼此之间传输数据),但似乎我错了。

现在我有一个我认为争议较小的解决方案(请给我你的意见):每次我在某些物理环境中使用 Vector 时,我都会考虑使用 enum :

enum Coord {
    x = 0,
    y = 1,
    z = 2
};
Vector V;
V[x] = 1;

我看到的唯一缺点是这些 x、y 和 z 可以在没有警告的情况下重新定义......

4

6 回答 6

11

这个应该通过查看反汇编来测量或验证,但我的猜测是:getter 函数很小,它的参数是恒定的。编译器很有可能会内联函数并对减法进行常量折叠。在这种情况下,运行时成本将为零。

于 2012-08-07T22:45:42.327 回答
3

为什么不试试这个:

class Vector {
    public:
        Vector(double x=0., double y=0., double z=0.) { 
            coordinates[1] = x;
            coordinates[2] = y;
            coordinates[3] = z;
        }
    private:
        double coordinates[4];
};

如果您没有以数百万的数量实例化您的对象,那么内存腰围可能是负担得起的。

于 2012-08-07T22:39:54.510 回答
1

您是否真的对其进行了分析或检查了生成的代码?这就是这个问题的答案。

如果 operator[] 实现是可见的,那么这很可能被优化为具有零开销。

于 2012-08-07T22:55:32.900 回答
1

如果没有基准测试,您就无法对性能发表任何客观的评价。在 x86 上,可以使用相对寻址来编译这种减法,这非常便宜。如果operator[]是内联的,那么开销为零——您可以使用inline或使用特定于编译器的指令(例如 GCC 的__attribute__((always_inline)).

如果您必须保证它,并且偏移量是编译时常量,那么使用模板就是要走的路:

template<size_t I>
double& Vector::get() {
    return coordinates[i - 1];
}

double x = v.get<1>();

出于所有实际目的,由于恒定折叠,这保证了零开销。您还可以使用命名访问器:

double Vector::x() const { return coordinates[0]; }
double Vector::y() const { return coordinates[1]; }
double Vector::z() const { return coordinates[2]; }

double& Vector::x() { return coordinates[0]; }
double& Vector::y() { return coordinates[1]; }
double& Vector::z() { return coordinates[2]; }

对于循环,迭代器:

const double* Vector::begin() const { return coordinates; }
const double* Vector::end() const { return coordinates + 3; }

double* Vector::begin() { return coordinates; }
double* Vector::end() { return coordinates + 3; }

// (x, y, z) -> (x + 1, y + 1, z + 1)
for (auto& i : v) ++i;

然而,像这里的许多其他人一样,我不同意你问题的前提。你真的应该简单地使用基于 0 的索引,因为它在 C++ 领域更自然。该语言已经非常复杂,对于将来维护您的代码的人来说,您不需要进一步复杂化。

于 2012-08-08T00:02:08.297 回答
1

我建议您在类的标题 (.h) 中定义它。如果您在 .cpp 中定义它,那么编译器将无法优化。此外,您的索引不应该是可以有负值的“int”......使其成为 size_t:

class Vector {

    // ...

public:
    double& operator[](const size_t i) {
        return coordinates[i-1] ;
    }

};
于 2012-08-07T23:09:33.870 回答
0

认真地,对这三种方式进行基准测试(即,将减法和 double[4] 方法与仅在调用者中使用从零开始的索引进行比较)。

在某些缓存架构上强制 16 字节对齐完全有可能您会获得巨大的胜利,同样有可能在某些编译器/指令集/代码路径组合上,减法实际上是免费的。

唯一的判断方法是对实际代码进行基准测试。

于 2012-08-07T23:00:28.967 回答