何时将 C++ 中的运算符重载为用户定义数据结构的成员,何时不重载?我完全迷失了

When to overload an operator in C++ as a member of a user-defined data structure and when not? I am completely lost

提问人:pk6x 提问时间:10/8/2023 最后编辑:pk6x 更新时间:10/9/2023 访问量:122

问:

对于任何可以帮助向我解释何时以及何时不将运算符重载为类或结构的成员或不作为成员(如全局)重载的人。

问题是,我正在学习所有关于向量和 3D 向量等的知识,用于数学学习目的,并尝试模拟它们的常见操作,以更好地理解它们的行为。

所以,我买了一本书,它稍微触及了与我相同的方面,但我到了这本书的作者用 C++ 编写一个简单的向量结构并得到一堆重载运算符的部分,其中一些在结构中声明和定义,有些则没有。

让我给你举一个让我完全困惑的例子。下面的示例将显示两个不同的重载运算符,它们两个通过它们执行相同的“总体”目的(我认为?),即(向量/标量乘法),但是,一个是结构体的成员,另一个不是。

struct Vector3D
{
    float x, y, z;

    Vector3D() = default;

    Vector3D(float a, float b, float c)
    {
        x = a;
        y = b;
        z = c;
    }

    Vector3D& operator*=(float s)
    {
        x *= s;
        y *= y;
        z *= z;

        return(*this);
    }

    Vector3D& operator/=(float s)
    {
        s = 1.0f / s;

        x *= s;
        y *= s;
        z *= s;

        return(*this);
    }

    float& operator[](int i)
    {
        return ((&x)[i]);
    }

    const float& operator[](int i) const
    {
        return ((&x)[i]);
    }
};


inline Vector3D operator*(const Vector3D& v, float s)
{
  return (Vector3D(v.x * s, v.y * s, v.z * s));
}

我特别要问的是

  Vector3D& operator*=(float s)
    {
        x *= s;
        y *= s;
        z *= s;

        return(*this);
    }

inline Vector3D operator*(const Vector3D& v, float s)
{
  return (Vector3D(v.x * s, v.y * s, v.z * s));
}

如果有人有明确的解释,请分享

我当然尝试了上面的代码,看看它们有什么区别,我发现它们有几处不同之处:

是当我尝试使用这两种方法将向量与标量(任何浮点数)相乘的最终结果打印到屏幕上时,只有成员重载运算符会立即打印出来。结构体外部的那个不能打印出来,除非返回值被分配给另一个向量实例。

例如:

int main()
{

    float s = 2; //Defining the scalar

    // Invoking the first overloaded operator

    Vector3D vecA(2, 2, 2);

    vecA *= 3;

    for(int i = 0; i < 3; ++i)
    {
        std::cout << vecA[i] << std::endl; // 6, 6, 6 (Worked)
    }

    // Now Invoking the global overloaded operator

    Vector3D vecB(2, 2, 2);

    vecB * s; 

    for(int i = 0; i < 3; ++i)
    {
        std::cout << vecB[i] << std::endl; // 2, 2, 2 (No change)
    }
    
}
C++ 数学 矢量图形

评论

0赞 UnholySheep 10/8/2023
vecB * s; 创建一个新的,然后丢弃它 - 它不会以任何方式修改Vector3DvecB
0赞 Sam Varshavchik 10/8/2023
您是否了解 C++ 之间的区别,它们的作用以及它们的不同之处?忘掉类吧,但是这两个运算符是如何在普通 s 上工作的?**=int
0赞 pk6x 10/8/2023
@UnholySheep 那么,让另一个重载运算符的目的是什么呢?我的意思是,如果第一个已经在做这项工作,那么为什么要有第二个呢?这似乎是许多教科书和在线教程的常见做法。
0赞 pk6x 10/8/2023
@SamVarshavchik我知道 *= 正在执行算术运算,并再次将新值重新分配给同一变量,作为语法上的糖方式。like 而不是必须写 vecB = vecB * s;我不知道还有什么要对你说实话的。
1赞 Pete Becker 10/8/2023
我对你正在使用的书的质量持严重保留态度。首先,应该使用而不是再次编写相同的代码:.其次,假装这是数组的第一个元素。事实并非如此。代码不需要做任何有意义的事情;它可能会偶然“工作”,但使用不同的编译器(包括具有不同构建开关的相同编译器),它可能会产生废话。或者更糟;行为未定义。operator*(const Vector3D& v, float s)*=Vector3D result = v; v *= s; return v;operator[]((&x)[i])x

答:

1赞 tbxfreeware 10/8/2023 #1

选择非成员算子的一个原因是对称性。所谓对称性,我的意思是某些算子应该是可交换的。

标量乘法就是一个很好的例子。想要两者和允许的表达式是合理的。但是,如果要用作左操作数,则必须使用非成员函数。不能使用成员函数,因为它不是对象。实际上,您必须定义两个函数,一个用于 ,另一个用于 。(s * vec)(vec * s)ssVector3Doperator*(s * vec)(vec * s)

流运算符是必须使用非成员运算符函数的另一种情况,因为左操作数不是 Vector3D 对象。

以下程序中的评论进一步解释。它包含几个您可能会发现有用的成语,包括隐藏的朋友成语。此外,该程序还修复了 @Pete Becker 发现的两个问题:

  1. operator*应通过调用 来实现。在两个不同的函数中对同一组操作进行编码很容易出错。迟早,两人会不同步!operator*=
  2. OP 中的函数调用未定义的行为,因为它们做出了毫无根据的假设,即成员变量 ,并且像数组的元素一样存储。下面的代码使用一个实际的数组。operator[]xyz
// main.cpp
#include <array>
#include <cstddef>
#include <format>
#include <iostream>

class Vector3D
{
    enum : std::size_t { zero, one, two, three };
    using value_type = double;
    std::array<value_type, three> v{};
public:
    value_type& x() noexcept { return v[zero]; }
    value_type& y() noexcept { return v[one]; }
    value_type& z() noexcept { return v[two]; }

    value_type const& x() const noexcept { return v[zero]; }
    value_type const& y() const noexcept { return v[one]; }
    value_type const& z() const noexcept { return v[two]; }

    Vector3D() noexcept
        = default;
    Vector3D(
        value_type const x,
        value_type const y,
        value_type const z) noexcept
    {
        this->x() = x;
        this->y() = y;
        this->z() = z;
    }
    Vector3D& operator*=(value_type const s) noexcept
    {
        // operator*= must be a member function.
        x() *= s;
        y() *= s;
        z() *= s;
        return *this;
    }
    friend Vector3D operator*(Vector3D v, value_type const s) noexcept
    {
        // Note that v is a value parameter that receives 
        // a copy of its argument. Changing v here, will not 
        // change the original argument used when operator* 
        // is invoked.
        //
        // This function could also be implemented as a 
        // member function (with only one parameter, s).
        //
        // It is a common idiom for operator* to be implemented 
        // by calling member function operator*= to do the work.
        // 
        // The following return statement can be broken into pieces:
        // 
        //    1. Parameter v is like a "local" variable. It is a 
        //       Vector3D object, but it is not *this object.
        //
        //    2. Because v is a Vector3D object, we can use 
        //       operator*= to perform scalar multiplication on it.
        //
        //    3. operator*= returns a reference to v, which now 
        //       holds the result of scalar multiplication.
        //
        //    4. The return statement copies the result, and 
        //       sends it back to the calling routine.
        return v *= s;
    }
    friend Vector3D operator*(value_type const s, Vector3D v) noexcept
    {
        // In order to have a symmetric operator*, where s 
        // can appear either first or second, we have to code 
        // a second version of operator*.
        //
        // This function cannot be a member function, because 
        // the first operand, s, is not an object!
        //
        // Note that the two operator* functions use the 
        // "hidden friend" idiom.
        return v *= s;
    }
    Vector3D& operator+=(Vector3D const& that) noexcept
    {
        // operator+= must be a member function.
        x() += that.x();
        y() += that.y();
        z() += that.z();
        return *this;
    }
    friend Vector3D operator+(Vector3D a, Vector3D const& b)
    {
        // operator+ can be implemented either as a 
        // member function (with one parameter), or as 
        // a non-member function (with two parameters).
        //
        // I have chosen to use a non-member function, 
        // so that I can treat the left operand as 
        // a "local" variable.
        //
        // Note that parameter `a` is a value parameter.
        // It receives a copy of its argument.
        //
        // Unlike scalar multiplication, there is no "symmetry" 
        // problem here, so I only need one version of 
        // operator+.
        //
        // You can do an internet search to learn more about 
        // the "hidden friend" idiom used here. I like the 
        // video by Dan Saks:
        // https://www.youtube.com/watch?v=POa_V15je8Y
        return a += b;
    }
    std::string to_string() const
    {
        return std::format("[{}, {}, {}]", x(), y(), z());
    }
    friend std::ostream& operator<< (
        std::ostream& ost,
        Vector3D const& v)
    {
        // Another hidden friend.
        //
        // Once again, this function must be implemented as 
        // a non-member function, because the left operand 
        // is not a Vector3D object.
        ost << v.to_string();
        return ost;
    }
    value_type& operator[](std::size_t const i) noexcept
    {
        // Trap subscripting errors.
        return v.at(i);
    }
    value_type const& operator[](std::size_t const i) const noexcept
    {
        // Trap subscripting errors.
        return v.at(i);
    }
};
int main()
{
    Vector3D a{ 1, 2, 3 };
    a *= 2;
    auto b = a * 3;
    std::cout
        << "a     : " << a
        << "\nb     : " << b
        << "\nb * 2 : " << b * 2
        << "\n2 * b : " << 2 * b
        << "\na + b : " << a + b
        << "\n\n";
    return 0;
}
// end file: main.cpp

输出:

a     : [2, 4, 6]
b     : [6, 12, 18]
b * 2 : [12, 24, 36]
2 * b : [12, 24, 36]
a + b : [8, 16, 24]

评论

0赞 pk6x 10/8/2023
感谢您的解释和建议的替代方案:-)。现在所有重载运算符都包含在一个数据结构中,这真是太好了!关于选择将向量设为类而不是结构并将 x、y 和 z 作为方法背后的决策,有一个问题?我可以在构造函数中看到您已将它们定义为双精度,但为什么不从一开始就这样做呢?是和退货有关吗?这是我最不熟悉的这一部分。实际上不是,这是在定义枚举和向下定义时。如果可以,请向我进一步解释,将不胜感激。public:
0赞 tbxfreeware 10/8/2023
最简单的修复方法是使用数组来保存三个向量分量。添加函数 , , 和 只是一点点合成糖,旨在让您使用自然的 x-y-z 符号。有了这个结构,就没有必要将数组设为公共,所以我从 切换到 ,并将数组设为私有。operator[]x()y()z()structclass
0赞 tbxfreeware 10/8/2023
这只是声明类型化常量的一种技巧方法。当枚举常量用于需要整数值的表达式时,它们将隐式转换为类型。enumstd::size_t
0赞 tbxfreeware 10/9/2023
@pk6x,您可能需要再次通读代码。我在一些解释中添加了更多细节。