为什么不能将重载运算符定义为类的静态成员?

53

C++语法允许在结构体/类内部定义重载操作符,例如:

struct X
{
   void operator+(X);
}

或者在结构体/类的外部,例如:

void operator+(X, X);

但不是这样:

struct X
{
   static void operator+(X, X);
}

有人知道这个决定的原因吗?为什么第三种形式不被允许?(MSVC会给出语法错误。)也许背后有一些故事?

另外,同时存在第一和第二定义会产生歧义:

1>CppTest1.cxx
1>c:\ballerup\misc\cf_html\cpptest1.cxx(39) : error C2593: 'operator +' is ambiguous
1>        c:\ballerup\misc\cf_html\cpptest1.cxx(13): could be 'void B1::operator +(B1 &)'
1>        c:\ballerup\misc\cf_html\cpptest1.cxx(16): or       'void operator +(B1 &,B1 &)'
1>        while trying to match the argument list '(B1, B1)'

我不明白为什么这种歧义比1,3或2,3之间的歧义更好。


4
重载静态运算符与友元或非成员函数相比有哪些不同用途? - James McNellis
3
这个设计决定背后的原因可能很有趣(如果有的话)。 - Luchian Grigore
1
void operator+(X);?难道你的意思不是 X& operator+(X) 吗?也许我是唯一一个感到困惑的人,所以如果我错了,请纠正我。 - E net4
2
@E_net4:在实际场景中你可能会这样做,但在编程语言中并不是必需的。 - GManNickG
5
@GManNickG 的 operator+ 应该返回 X,而不是 X& - fredoverflow
显示剩余7条评论
9个回答

20

因为没有明显的语法来调用这样的运算符,这意味着我们必须想出一个东西。请考虑以下变量:

X x1;
X x2;

现在,我们假设一下使用普通成员函数而不是运算符-比方说我在你的例子中将operator+改为plus

这三个调用都会变成:

x1.plus(x2);
plus(x1, x2);
X::plus(x1, x2);
现在,当使用+进行操作符调用时,编译器如何知道要在X的范围内查找您的运算符?对于普通的静态成员函数来说,它无法这样做,而且操作符也没有特别的权力来消除歧义。
现在考虑如果你的程序中同时声明了第二和第三个形式。如果你说x1 + x2,编译器将不得不始终选择自由函数或调用将是不明确的。唯一的真正替代方案将是类似于x1 X::+ x2这样的东西,看起来很丑陋。鉴于这一切,我相信标准委员会决定简单地禁止静态成员版本,因为它能完成的任何事情都可以用友元自由函数代替。

如果您同时拥有第二和第三种形式,那该怎么办呢?如果我同时拥有第一和第二种形式呢?我感觉不出区别... - Kirill Kobelev
"x1+x2",并且只有第一种形式的重载。编译器如何找到它?如果可以找到,为什么不能找到同一类的其他成员? - Kirill Kobelev
12
这种推理是错误的。如果重载运算符存在于静态函数中,它们将处于类作用域内。从该类作用域内,可以像通常一样调用它们。从作用域外部,您可以使用作用域解析语法以及显式的运算符调用:foo_class::operator +(a, b) - Kaz
1
如果它没有不断地谈论“编译器”,那么这将是最权威的答案,因为编译器与所有这些都没有任何关系。 - Kerrek SB
3
我的最终结论是运算符重载只是语法糖,可以通过显式调用来调用静态运算符,这正好违反了语法糖的概念,因此这就是不允许静态运算符重载的原因。我选择你的答案作为最能强调这个观点的答案,尽管我认为operator()应该有一个单独的章节来讨论。 - Antonio

17

我对任何有关此概念的C++讨论都没有具体的知识,所以可以无视这一点。

但是对我来说,你把问题想反了。问题应该是“为什么允许使用这种语法?”

它与当前的语法相比并没有任何优势。非静态成员函数版本具有与您拟议的静态版本相同的访问私有成员的权限。因此,如果您需要访问私有成员来实现它,请将其设置为非静态成员,就像通常对类的大多数成员所做的那样。

它不会使实现非对称运算符(例如:operator+(const X &x, const Y &y))更容易。如果您需要私有访问来实现此操作,则仍需要在其中一个类中对它们进行友元声明。

因此,我会说它不存在的原因是它没有必要。在非成员函数和非静态成员之间,所有必要的用例都得到了覆盖。


或者换句话说:

自由函数可以做到静态函数系统所能做的一切,而且更多。

通过使用自由函数,您可以在模板中使用的运算符上实现参数相关的查找。使用静态函数无法做到这一点,因为它们必须是特定类的成员。而且您无法从类外部添加类,而可以将其添加到命名空间中。因此,如果需要将运算符放入特定命名空间以使某些ADL代码工作,可以这样做。静态函数操作符不能做到这一点。

因此,自由函数是您拟议的静态函数系统提供的所有内容的超集。由于允许它没有任何好处,因此没有理由允许它,因此不允许。


这将使得可以在不实例化functor的情况下使用它们?

这是一个自相矛盾的说法。“functor”是“函数对象”。类型不是一个对象,因此它不能是一个函数对象。它可以是一种类型,当实例化时会产生一个函数对象。但仅有类型本身不会成为函数对象。
此外,能够声明Typename::operator()静态并不意味着Typename()会做你想要的事情。该语法已经有了实际含义:通过调用默认构造函数来实例化Typename临时对象。
最后,即使所有这些都不是问题,那真正有什么好处呢?大多数接受某种可调用类型的模板函数与函数指针一样有效,而不仅仅是函数对象。为什么要限制你的接口,不仅仅是到函数对象,而是到不能具有内部数据的函数对象?这意味着您将无法传递捕获lambda等。
一个不可能包含状态的函数对象有何用处?为什么要强制用户传递没有状态的“函数对象”?为什么要阻止用户使用lambda?
因此,你的问题基于错误的假设:即使我们拥有它,也不会给你想要的东西。

11
自由函数可以完成静态函数系统的所有任务,甚至更多。但这也是不要使用静态函数的一个论据。 - Kaz
4
静态类函数唯一的目的是为了使用 class::function() 这种语法。如果你真的想要输入 class::operator+(A, B),那么使用 static 就有意义了,但...... - Mooing Duck
2
@Kaz:我的理解是静态成员函数早于命名空间出现;并且被添加到语言中以实现类似的名称冲突防止。但我可能错了。 - Billy ONeal
@NicolBolas
  1. 我不需要将函数对象存储以供以后使用。
  2. 在某些情况下,函数对象的目的非常小,对用户的好处是他可以少传递一个参数给函数,并且实例化类时可减少一行代码。
  3. 我暂时不能使用C+11,所以还没有研究lambda表达式 :)
  4. 但最终我理解到,对函数对象类进行如此多的限制,我也可以限制传递包含某个名称静态方法的类...与编写编译器的人所付出的努力相比,这是微不足道的额外负担...
- Antonio
如何区分构造函数和 operator(),因为如果我必须显式调用 ::operator(),那么代码的优雅性就没有任何提升。 - Antonio
显示剩余13条评论

4

嗯...我在考虑一个静态运算符(),它可以隐式删除所有构造函数...那将会给我们一些类型化的函数。有时候我希望C++里也有这个。


https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p1169r2.html :)。我希望我提出了这个建议,但我猜Barry做得更好。 - Slava

4
静态成员函数可用于开发实用程序,这些程序有助于类的实现,但由于某种原因不是成员。可以想象,在表示为静态类成员函数的实用程序中,拥有运算符可能很有用。当然,如果某个重载运算符将类C作为其主要参数,则没有理由希望它成为类C的静态成员。它可以是非静态成员,因此可以隐式获得该参数。但是,类C的静态成员可能是在除C之外的一些类上重载的运算符。假设存在一个文件作用域 operator ==(const widget &, const widget &)。在我的 squiggle 类中,我正在使用 widget 对象,但想要进行不同的比较。我应该能够为自己创建一个 static squiggle::operator == (const widget &, const widget &)。从类作用域来说,这很容易调用。
void squiggle::memb(widget a, widget b)
{
   if (a == b) { ... } // calls static == operator for widgets
}

在类范围之外,我们只能使用显式作用域解析结合显式操作符调用语法来调用它:

void nonmemb(widget a, widget b)
{
   a == b;  // calls the widget member function or perhaps nonstatic operator
   squiggle::operator ==(a, b); // calls squiggle class' utility
}

这不是一个坏主意。此外,我们可以使用常规的重载函数实现它,只是不能使用运算符。如果使用 compare 函数比较小部件,则可以有一个非成员的 compare 函数或 widget::compare,还可以有一个接受 widgetssquiggle::compare
因此,这个想法中唯一不受 C++ 支持的方面是运算符的语法糖处理。
也许这并不是一个足够有用的想法,不值得支持(至少目前如此!)。毕竟,这不会允许对 C++ 程序进行革命性的重组。但它可以修复语言上的一个问题。
另外,请注意类运算符 newdelete 的重载是隐式静态的!因此,该问题已经存在一些例外。

1

虽然我晚了几年,但我想提供一个与其他答案不同的观点。考虑设计适用于实现支持的各种ABI的特征类的情况。例如,考虑为simd ABI设计特征类的情况:

template<typename T, typename Abi>
struct abi_traits {};

template<>
struct abi_traits<float, sse>
{
    using vector_type = __m128;

    [[nodiscard]] static constexpr auto operator&(auto a, auto b) noexcept
    {
        return _mm_and_ps(a, b);
    }
};

然后您就可以为每个abi调用运算符重载:

abi_traits<T, Abi>::operator&(a, b);

这可能看起来有些牵强,但考虑一下使用命名函数而不是运算符重载的替代方案:

abi_traits<T, Abi>::and(a, b);

你可能已经知道 "and" 是 C++ 中的一个关键字,因此这段代码无法编译。你需要以某种方式修改名称才能使其工作:
abi_traits<T, Abi>::op_and(a, b);

那么从您的向量类中,您需要像这样提供实现:

template<typename T, typename Abi>
struct simd
{
    ...
    constexpr auto operator&(auto b) noexcept
    {
        traits<T, Abi>::op_and(this->value, b);
    }
};

你当然可以这样做,但是那样你就需要维护和理解程序员强加的混乱命名语法。在我看来,允许静态重载更清晰明了,因为这将更好地展示代码意图:

template<typename T, typename Abi>
struct simd
{
    ...
    constexpr auto operator&(auto b) noexcept
    {
        traits<T, Abi>::operator&(this->value, b);
    }
};

所以……我理解其他答案对此持谨慎态度的原因,但我认为在可能的情况下具有静态运算符重载是有价值的。我曾经看到过一些提案,旨在添加静态运算符重载(例如,this proposal for static operator())。也许这值得进行更广泛的讨论,甚至可以提出建议。我遇到过许多情况,自己想要拥有这个功能,因为命名很难,这是一个被低估的问题。如果我看到一个形如op_shl(auto a, auto b)的函数,我会比看到一个形如operator<<(auto a, auto b)的函数更不确定该如何处理这个函数。我们可以就软件设计编写良好的注释并遵循最佳实践进行争论,但归根结底,静态运算符语法没有歧义。

1
我对这里所有糟糕的回答感到非常惊讶。大多数都基于错误的前提(“因为每个操作符都需要一个操作数”),或者他们简单地声称,“它是被禁止的,因为它是无用的。” 这后一点可能是委员会的推理,但这是一个非常任意的规则的可怕理由。在我的情况下,我想要一个 Singleton 类拥有一个 operator ->,这将确保单个实例在静态初始化混乱期间被实例化。 - Phil Hord
我在这里有另一个用例 https://dev59.com/y3E85IYBdhLWcg3wyGl_#74669964(这是关于命名空间与结构体的讨论)。 - az5112

1

首先,我不确定为什么 C++ 不允许使用静态运算符。作为 Python 程序员,我看到了一些很好的 API 灵活性示例,使用 @classmethod 方法,这些方法被称为 Class.method,似乎没有人因此而受苦。

我的猜测是,在 C++ 中,这可能与语言设计相关,因为至少我没有看到其他任何阻止它发生的东西。

现在,即使你不能合法地这样做,你也可以使用 #define 和一些运气来欺骗它) 免责声明!:也许你不应该在家里这样做,但这取决于你自己


#include <iostream>

// for demonstration purposes
// no actual array implementation
class Array
{
public:

  Array() { std::cout << "Array() created\n"; }

  Array operator()()
  {
    std::cout << "surprising operator() call\n";
    return Array();
  }

  int operator[] (int elem_count)
  {
    return elem_count;
  }

};

#define Array Array()

int main()
{
  // this is not a static method, right, but it looks like one. 
  // and if you need the static-method syntax that bad, you can make it. 
  auto arr = Array[7]; // implicitly acts as Array()[N]
  auto x = Array(); // delegate all construction calls to Array.operator()
  std::cout << arr;
}


所以,我认为你可能会通过这种方式重载其他运算符,并使其在语法上看起来像是静态的。


0

基本上,类成员的静态运算符在非静态成员上没有任何优势。

对于一个类定义的任何运算符,都必须至少有一个参数是该类类型。

成员运算符以隐式的this参数的形式接收该参数。

非成员运算符有一个明确的该类类型的参数。

运算符函数的接口不关心这些;当我们调用a + b时,它会根据需要生成代码来通过this参数或显式声明的参数传递a。因此,无论运算符是静态的还是非静态的,在使用运算符时并没有表达出任何细微差别。

假设突然出现一个要求,即最新的 ISO C++ 必须支持静态成员运算符。如果匆忙实现此要求,可以按照以下模式进行源码重写:

static whatever class::operator *(class &x) { x.fun(); return foo(x); }

-->

whatever class::operator *() { (*this).fun(); return foo(*this); }

-->

whatever class::operator *() { fun(); return foo(*this); }

编译器将static成员运算符重写为非静态的,删除最左边的参数,并(在遮蔽方面具有适当的词法卫生)用表达式*this替换对该参数的所有引用(不必要使用可以省略)。

这种转换足够简单,以至于程序员可以信任地一开始就以这种方式编写代码。

static运算符函数定义机制不够强大。例如,它不能是virtual,而非静态的则可以。


你做出了一个假设,即两个操作数的类型相同。如果我需要定义 std::ostream& operator<<(std::ostream&, whatever const&) 该怎么办呢? - az5112
@az5112 是的,完全正确;我在2013年回答这个问题时就注意到了这一点。非常奇怪;为什么我五年后又写了这个答案。就在大约24小时前,我还在看这个答案,想知道是否要删除它。 - Kaz

0

我不知道允许静态运算符+可能会带来任何直接的缺点(也许仔细思考一下会产生一些理论)。但我认为,至少Bjarne Stroustrup宣布的“不为你不使用的东西付费”的原则已经足够好的答案了。 如果允许使用静态运算符,除了更复杂的语法(你必须到处写“X::operator+”而不是“+”),你会得到什么?


1
运算符括号怎么用于函数对象? - Antonio
为什么我需要编写 X::operator+?在其他情况下,它应该推断出正确的运算符重载... - Kirill Kobelev
@Kirill 对于类的任何静态成员函数,要调用它,必须使用::指定类名。如果您不是定义禁止的静态operator+,而是编写允许的静态方法Add(X&,X&),那么您将如何调用它?您需要编写X :: Add(x,y)。如果您有一个静态成员变量,比方说双倍高度。如何访问它?您必须编写X :: myHeight。如果运算符有可能出现,则同样适用。要引用类的静态成员,必须编写类名,后跟::。这是C ++语法。 - mvidelgauz
顺便提一句,Kaz已经在他/她的评论中(用更多的C++术语!)提到了这一点。4月12日22:09。 - mvidelgauz
根据你的逻辑,当“Add”是X类的静态成员且x和y都是X类型的对象时,只需编写Add(x,y)就可以了 - 编译器必须能够“在类中找到实例成员”。这也在技术上是可能的 - 就像你的静态运算符+一样。但是语言设计禁止这样做。因为根据设计,任何静态成员都必须使用类名和::引用。 - mvidelgauz
显示剩余5条评论

0

这可能是原因。

因为每个运算符都需要一个或多个操作数。所以如果我们将其声明为静态,那么我们就不能使用对象(操作数)调用它。

为了在一些操作数上调用它,这些操作数仅仅是对象,函数必须是非静态的。

下面是在进行函数重载时必须满足的条件。

  • 它必须至少有一个用户定义类型的操作数。

所以假设我们将我们的运算符重载函数声明为静态的。 那么首先以上条件将不会被满足。

另一个原因是,在静态函数内部,我们只能访问静态数据成员。但是在进行运算符重载时,我们必须访问所有数据成员。因此,如果我们将我们的运算符重载函数声明为静态的,我们无法访问所有数据成员。

因此,运算符重载函数必须是一个非静态成员函数

但是有一个例外。

如果我们使用友元函数进行运算符重载,则可以将其声明为静态。


静态方法可以有对象作为操作数。问题出在哪里? - Kirill Kobelev
假设您有Obj x3 = x1 + x2; 那么如果您没有任何对象,如何调用+函数? - Narendra
虽然静态成员函数不能直接访问类的实例成员,但是一旦它有了一个实例,就可以访问它们。您可以编写 static void operator+(X &x1, X &x2) { x1.NonStaticMethod1(x2.NonStaticDataField); }。x1和x2是实例。这意味着静态方法可以访问它们的实例成员,包括受保护/私有成员。操作符的实现除此之外不需要任何东西。 - Kirill Kobelev
1
在 ISO C++ 中,运算符重载不能是静态类成员函数。也许你的编译器允许它作为扩展。GNU g++ 4.6.1:test.cc:5:48: error: ‘static bool foo::operator==(bar&, bar&)’ must be either a non-static member function or a non-member function。无论我们将第一个参数设置为 foo &,都没有关系。 - Kaz
如果我们在 static 中添加 friend 关键字,它就不再是静态成员函数了!它是一个非成员函数,而 static 关键字则具有与 C 中紧密相关的通常含义。 - Kaz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接