【问题标题】:Virtual-like friend functions?类似虚拟的朋友功能?
【发布时间】:2025-12-09 09:30:01
【问题描述】:

我想创建类似的界面

class Scalar {
public:
  Scalar() {}
  virtual ~Scalar() {}
  //virtual members operators
  virtual Scalar& operator+() const = 0;
  virtual const Scalar operator-() const;
  virtual Scalar& operator=() = 0;
  virtual Scalar& operator+=() = 0;
  //...
};

我也打算使用一些朋友功能,例如:

    friend const Scalar operator+(const Scalar&, const Scalar&);

但是当我派生抽象类并创建派生类时出现问题,比如说:

class RealNumber: public Scalar {
public:
  friend const RealNumber operator+(const RealNumber&, const RealNumber&);
  //some definitions...
};

根据这个逻辑,我需要为从Scalar 派生的每个新类定义一个新的朋友operator+ 重载。有没有办法解决这个问题并避免在所有派生类中声明这些朋友?

【问题讨论】:

  • 这是XY problem
  • 如果在派生类中使用友元函数,您还可以访问从基类继承的受保护成员。但这只有在访问变量的静态类型来自(在这种情况下)Y 而不是 X 的多态变量(就像在你的情况下)时才有效。
  • 您通常不希望首先在层次结构中重载类型的运算符。这些设计在实践中很容易崩溃。缺乏多重分派只是造成这种情况的原因之一。
  • 为什么是好友功能??您可以创建一个虚拟 + 运算符。但这不是最好的方法
  • 我想你会发现这种方法会充满错误和问题。它不仅仅是 C++,而是在我熟悉的每一种 OOP 语言中。

标签: c++ polymorphism operator-overloading abstract-class friend


【解决方案1】:

这是你的问题吗?

我知道你的问题是你的两个朋友指的是完全不同的函数,因为他们有不同的签名:

friend const Scalar operator+(const Scalar&, const Scalar&);
friend const RealNumber operator+(const RealNumber&, const RealNumber&);

更糟糕的是,类外部友元的选择不会是多态的:将根据编译时类型选择正确的友元。

如何解决?

首先,您可以考虑覆盖类本身的运算符(保持签名相同),而不是使用外部重载的朋友。

但这有两个主要挑战:

  • 几乎不可能从算术运算符返回引用,除非您接受副作用,否则会使运算符的行为与预期不同。
  • 您需要处理组合不同类型的标量:如果您必须将 Scalar 添加到 RealNumber 怎么办?这需要实现double dispatch 以应对所有可能的组合。

那么,死路一条?

不,还有其他两种选择,具体取决于您的实际问题:

  • 您真的想在运行时动态组合算术类型吗?如果是,您需要放弃 C++ 运算符覆盖方法,并使用 interpreter pattern 实现表达式求值器。
  • 如果不是,请考虑使用基于模板的设计,以便编译器在编译时选择适当的专业化。
  • 或与许多可能的朋友及其参数类型的组合一起受苦。

【讨论】:

  • 谢谢,是的 - 这是我的问题 :) 我使用过模板。
【解决方案2】:

您可能无法创建虚拟好友函数,但您可以创建虚拟运算符(甚至operator + 也可以这样做)。

考虑以下代码:警告:这根本不是好的设计

#include <iostream>
using namespace std;

class AA {
private:
    int a;
public:
    AA(int a): a(a) {};
    inline int getA() const { return a; };
    virtual AA operator +(const AA &a) {
        AA res(this->getA() + a.getA());
        return res;
    }
};

class BB: public AA {
    public:
    BB(int a): AA(a) {}
    virtual AA operator +(const AA &a) {
        AA res(this->getA() - a.getA());
        return res;
    }
};

int main() {
    BB tmp(1);
    AA& a = tmp;
    AA b(7);
    cout << (a + b).getA();
    return 0;
}

当我写这段代码的时候,我发现可能会导致很多缺陷(比如真正做减法的 + 运算符,如果第二个操作数是 BB 而不是第一个呢??)

所以,关于您的问题,您需要标量。因此,您可以执行以下方法:

#include <iostream>
using namespace std;

// Here, Scalar acts as an abstract class, all its goal is to convert your
// class type into some calculable value type (e.g. you can use T as double)
template <typename T>
class Scalar {
public:
    // Converter function is abstract
    virtual operator T() = 0;
};

class AA: public Scalar<double> {
private:
    double a;
public:
    inline double getA() {return a;};
    AA(double a): a(a) {}
    // Implements the converter function in Scalar, T in scalar is mapped
    //    to double because we did Scalar<double>
    virtual operator double() {
        return a;
    }
};

class BB: public Scalar<double> {
private:
    int a;
public:
    inline double getA() {return (double)a;};
    BB(int a): a(a) {}
    virtual operator double() {
        return (double)a;
    }
};

int main() {
    BB tmp(1);
    AA b(7);
    // Here, it is easy for us just to add those, they are automatically converted into doubles, each one like how it is programmed.
    cout << (b + tmp);
    return 0;
}

【讨论】:

  • 谢谢,但这并不能解决我的问题。我想要的是有有限的字段来操作,所以覆盖 operator+ 和 operator* 非常重要。我发现,我可以部分专门化模板,这非常有用(如果你做得聪明,它可以使用后期绑定!)。谢谢你的回答:)
  • @Maciej 真的模板更好,因为virtual 函数是动态调度,这是老派的东西,绑定到Java/Objective-C 和其他慢速语言,而模板是关于静态的编译时解析,类似于现代语言 Swift 和 Rust
  • 感谢您的提示:)