2

我有以下棘手的问题:我已经实现了一个(相当复杂的)类,它在多小波基础上表示数学函数。由于像 +、- 和 * 这样的操作在这种情况下是很自然的,所以我为这个类实现了重载运算符:

FunctionTree<D> operator+(FunctionTree<D> &inpTree);
FunctionTree<D> operator-(FunctionTree<D> &inpTree);
FunctionTree<D> operator*(FunctionTree<D> &inpTree);

这些运算符在简单的非链接操作中工作得更好,甚至在某些情况下链接运算符时。像这样的陈述

FunctionTree<3> y = a * b + c;
FunctionTree<3> z = a * b + b;

编译并且看起来工作正常。第一行实际上没问题,但第二行让 valgrind 告诉你关于在已释放区域内释放内存和访问未初始化变量的可怕故事。此外,像这样的声明

FunctionTree<D> y = a + b * c;

甚至不会编译,因为我还没有定义(一个模棱两可的运算符将一个实际的对象,而不是一个引用作为参数)。当然,解决方案很明确:所有参数和方法都应该设为 const,甚至可能返回一个 const 对象或引用。不幸的是,这是不可能的,因为在操作期间所涉及的对象都不是恒定的!这听起来可能很奇怪,但这是所涉及的数学不可避免的结果。我可以伪造它,使用 const_cast,但代码仍然是错误的!

有没有办法解决这个问题?我目前唯一的解决方案是将返回对象设为 const,从而有效地禁用运算符链接。

问候,.jonas。

4

5 回答 5

5

如果您的对象是 1GB(我猜这是它们在堆上分配的内存,而不是它们的实际sizeof大小),那么您可能不应该在它们上支持这些运算符。问题是您的运算符链接示例或多或少假定不可变对象作为其“正在发生的事情”的基本模型,并创建许多临时对象作为中间结果。您不能指望编译器能够有效地重用空间。但是你也不能随意复制 1GB 的对象。

相反,您应该只支持各种分配运算符。然后你的客户而不是写:

y = a * b + c;

这可能会产生巨大的临时性,而是应该写:

// y = a * b + c
y = a;
y *= b;
y += c;

这样用户就可以控制事情。很容易看出没有创建临时对象,而且您不会意外编写需要 18GB 才能运行的简单代码行。如果你想做:

y = a * b + c * d;

那么您的代码必须明确指出此处需要一个临时结果(假设您不能丢弃 a、b、c、d 中的任何一个):

// y = a * b + c * d
y = a;
y *= b;
{
   FunctionTree x = c;
   x *= d;
   y += x;
}

但是如果调用者碰巧知道在此之后不需要例如 c ,您可以明确地执行以下操作:

// y = a * b + c * d  [c is trashed]
c *= d;
y = a;
y *= b;
y += c;

从理论上讲,编译器可能会根据带有链接运算符的大表达式自行解决所有这些问题,并通过数据流分析来显示这些c内容是未使用的。开启了很多优化的优秀编译器在整数运算方面很擅长,所以机器就在那里。在实践中,我不会指望它。如果编译器不能证明 FunctionTree 的构造函数和析构函数没有可观察到的副作用,那么它跳过它们的能力仅限于标准中合法的“复制省略”的特定情况。

或者您可以查看 GMP 的 C 接口,以了解如何在完全没有操作员重载的情况下完成此操作。那里的所有函数都带有一个“输出参数”,结果被写入其中。所以例如如果x是一个巨大的多精度整数,你想乘以 10,你选择是否写:

mpz_mul_ui(x, x, 10); // modifies x in place, uses less memory

或者:

mpz_t y;
mpz_init(y);
mpz_mul_ui(y, x, 10); // leaves x alone, result occupies as much memory again.
于 2010-03-11T12:15:06.723 回答
2

这个问题没有简单的解决方案。您的二元运算符正在(正确地)生成无名临时对象 - 此类对象不能绑定到非常量引用参数。

解决此问题的一种可能方法是放弃运算符的链接 - 对于 X 类:

X a;
X b;
X c = a * b;
X d;
X e  = c + d;

另一个(相当可怕的)解决方案是使类的数据成员可变- 这样它们在逻辑上是 const 但在物理上是可变的。然后,您将使您的参数成为 const 引用。就像我说的,这很可怕,可能会导致其他问题。

于 2010-03-11T11:49:00.673 回答
1

“......在操作过程中,所涉及的对象都不是恒定的!这听起来可能很奇怪......” 不,这听起来并不奇怪,听起来是错误的。如果您的操作员以一种可以从外部观察到的方式修改对象,那么这就是对操作员重载的滥用。如果对象被修改,因为结果被缓存,使缓存可变,所以修改缓存的函数仍然可以声明为 const。

于 2010-03-11T12:07:21.433 回答
1

您可以使用代理而不是实际值,并且代理可以是常量,因为它们不会被更改。下面是一个小例子,说明它的外观。请注意,在该示例中仍将创建所有临时对象,但如果您想聪明一点,您可以只保存操作,而不是操作的实际结果,并且仅在有人想要最终获得结果或部分时才计算其中。它甚至可以极大地加速你的代码,因为它帮助了 APL

此外,您可能希望将大多数成员设为私有。

#include <memory>
#include <iostream>

struct FunctionTreeProxy;
struct FunctionTree;

struct FunctionTreeProxy {
    mutable std::auto_ptr<FunctionTree> ft;

    explicit FunctionTreeProxy(FunctionTree * _ft): ft(_ft) {}
    FunctionTreeProxy(FunctionTreeProxy const & rhs): ft(rhs.ft) {}

    FunctionTreeProxy operator+(FunctionTree & rhs);
    FunctionTreeProxy operator*(FunctionTree & rhs);
    FunctionTreeProxy operator+(FunctionTreeProxy const & rhs);
    FunctionTreeProxy operator*(FunctionTreeProxy const & rhs);
};

struct FunctionTree {
    int i;
    FunctionTree(int _i): i(_i) {}
    FunctionTree(FunctionTreeProxy const & rhs): i(rhs.ft->i) {}

    FunctionTree * add(FunctionTree & rhs) {
        return new FunctionTree(i + rhs.i);
    }

    FunctionTree * mult(FunctionTree & rhs) {
        return new FunctionTree(i * rhs.i);
    }

    FunctionTreeProxy operator+(FunctionTree & rhs) {
        return FunctionTreeProxy(add(rhs));
    }

    FunctionTreeProxy operator*(FunctionTree & rhs) {
        return FunctionTreeProxy(mult(rhs));
    }

    FunctionTreeProxy operator+(FunctionTreeProxy const & rhs) {
        return FunctionTreeProxy(add(*rhs.ft));
    }

    FunctionTreeProxy operator*(FunctionTreeProxy const & rhs) {
        return FunctionTreeProxy(mult(*rhs.ft));
    }
};

FunctionTreeProxy FunctionTreeProxy::operator+(FunctionTree & rhs) {
    return FunctionTreeProxy(ft.get()->add(rhs));
}

FunctionTreeProxy FunctionTreeProxy::operator*(FunctionTree & rhs) {
    return FunctionTreeProxy(ft.get()->mult(rhs));
}

FunctionTreeProxy FunctionTreeProxy::operator+(FunctionTreeProxy const & rhs) {
    return FunctionTreeProxy(ft.get()->add(*rhs.ft));
}

FunctionTreeProxy FunctionTreeProxy::operator*(FunctionTreeProxy const & rhs) {
    return FunctionTreeProxy(ft.get()->mult(*rhs.ft));
}

int main(int argc, char* argv[])
{
    FunctionTree a(1), b(2), c(3);

    FunctionTree z = a + b * c;

    std::cout << z.i << std::endl;

    return 0;
}
于 2010-03-11T12:37:41.393 回答
0

通过值而不是通过引用传递参数?

编辑:您可能想改用 += -= *= 。

于 2010-03-11T11:49:18.370 回答