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

vyswwuz2  于 2022-12-05  发布在  其他
关注(0)|答案(9)|浏览(219)

C++语法允许在结构/类内部定义重载运算符,如:

struct X
{
   void operator+(X);
}

或在结构/类之外,如:

void operator+(X, X);

而不是作为:

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

有人知道这个决定的原因吗?为什么不允许第三种形式?(MSVC给出了语法错误。)也许这背后有什么故事?
P.S.第一个和第二个定义同时出现会产生歧义:

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之间的模糊性更好。

nmpmafwu

nmpmafwu1#

因为没有一个明显的语法来 call 这样一个操作符,这意味着我们必须编造一些东西。

X x1;
X x2;

现在,假设我们使用的是普通成员函数而不是运算符--假设我在示例中将operator+更改为plus
这三个调用中的每一个都类似于:

x1.plus(x2);
plus(x1, x2);
X::plus(x1, x2);

现在,当使用+进行运算符调用时,编译器如何知道在X的作用域中查找运算符呢?它不能为普通的静态成员函数执行此操作,而且运算符也没有得到消除歧义的特殊豁免。
现在考虑一下,如果你在程序中声明了第二种和第三种形式。如果你说x1 + x2,编译器要么总是选择free函数,要么调用就不明确。唯一真实的选择是x1 X::+ x2,看起来很难看。考虑到所有这些,我确信标准委员会决定简单地禁止静态成员版本,因为它所能完成的任何事情都可以用一个朋友免费函数来代替。

l3zydbqr

l3zydbqr2#

我没有任何关于这个概念的C++讨论的具体知识,所以请随意忽略这一点。
但对我来说,你把问题倒过来了,问题应该是,“为什么这个语法是 allowed?”
非静态成员函数和静态成员函数对私有成员的访问是一样的,所以如果你需要访问私有成员来实现它,就把它变成一个非静态成员,就像你通常对类的大多数成员所做的那样。
它并不能使实现非对称运算符(即:operator+(const X &x, const Y &y)).如果你需要私有访问来实现它,你仍然需要在其中一个类中为它们声明一个friend.
所以我会说,它不存在的原因是它不是 * 必要的 。在非成员函数和非静态成员之间,所有必要的用例都被覆盖了。
或者,换一种说法:
自由函数可以做静态函数系统所能做的一切,
甚至更多 *。
通过使用自由函数,你可以对模板中使用的操作符进行依赖于参数的查找。你不能对静态函数这样做,因为它们必须是一个特定类的成员。而且你不能从类的外部 add 到一个类,而你可以添加到一个命名空间。所以如果你需要把一个操作符放在一个特定的命名空间中,以使一些ADL代码工作,你可以。你不能用静态函数运算符来做。
因此,自由函数是静态函数系统所能提供的一切的超集,因为允许它没有好处,也没有理由允许它,所以它是不允许的。
这使得在不示例化函子的情况下使用函子成为可能?
这是一个矛盾的术语。一个“函子”是一个“函数对象”。一个类型是 * 不是一个对象 *;因此,它不可能是函子。2它可以是一个类型,当示例化时,它将产生一个函子。3但是这个类型本身将不是函子。
此外,能够声明Typename::operator()为static并不意味着Typename()就能做你想做的事情。通过调用默认构造函数示例化Typename临时变量。
最后,即使不是这样,那又有什么用呢?大多数接受某种类型的可调用对象的模板函数与函数指针和函子一样工作。为什么要限制接口,不仅限于函子,还限于 * 不能 * 有内部数据的函子呢?这意味着你不能通过捕获lambda等等。
一个不可能包含状态的函子有什么用呢?为什么要强制用户传递没有状态的“函子”呢?为什么要阻止用户使用lambda呢?
所以你的问题是从一个错误的假设推导出来的:即使我们有,它也不会给予你你想要

ws51t4hk

ws51t4hk3#

静态成员函数可用于开发实用程序,这些实用程序有助于实现类,但由于某种原因不是成员。
很容易想象,在表示为静态类成员函数的实用程序中,使用运算符可能会很有用。
当然,如果某个重载运算符将类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函数来比较部件,那么可以有一个非成员comparewidget::compare,也可以有一个squiggle::compare接受widgets
因此,C中唯一不支持的方面是使用操作符的语法语法。
也许到目前为止,这还不是一个值得支持的有用的想法。毕竟,这并不是一个允许对C
程序进行革命性重组的想法,但它可以修复语言中的一个不完整性。
另外,考虑到newdelete操作符的类重载是隐式静态的!所以不完整性已经有了一点例外。

jhiyze9q

jhiyze9q4#

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

bxjv4tth

bxjv4tth5#

我晚了几年,但我想提供一个与其他答案相对照的观点。考虑一下为实现所支持的各种ABI设计一个traits类的情况。例如,考虑一下为simd ABI设计一个traits类的情况:

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);

因此,在您的vector类中,您必须提供如下实现:

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

你当然可以这样做,但是你必须维护和理解程序员强加的mangled-name语法。在我看来,允许静态重载要清楚得多,因为这样可以更好地显示代码的意图:

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())。也许这值得更大的讨论,甚至是一个proprosal。我确实遇到过很多这样的情况,我认为有了这个特性会更清楚。命名是一个困难的问题,而且它被低估了。如果我看到一个形式为op_shl(auto a, auto b)的函数,如果我看到一个如下形式的函数,我就不太确定该如何处理这个函数:operator<<(auto a, auto b)。我们可以提出所有关于编写好的注解和遵循软件设计的最佳实践的论点,但最终,静态运算符语法没有歧义。

c3frrgcw

c3frrgcw6#

这可能就是原因。
因为每个operator需要一个或多个operands,所以如果我们将它声明为static,那么我们不能使用对象(操作数)调用它。
为了在某个操作数上调用它,这个操作数只是一个对象,这个函数必须是非静态的。
下面是执行函数重载时必须满足的条件。

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

因此,假设我们声明我们的运算符重载函数为静态的,那么上述条件中的第一个将不满足。
另一个原因是,在静态函数中,我们只能访问静态数据成员。但在进行运算符重载时,我们必须访问所有数据成员。因此,如果我们将运算符重载函数声明为静态,我们就不能访问所有数据成员。
所以运算符重载函数必须是non-static member function
但有一个例外。

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

col17t5w

col17t5w7#

我不知道允许使用静态运算符+会导致什么直接的缺点(也许思考足够长的时间会产生一些理论)。但我认为至少Bjarne Stroustrup所宣称的“不要为你不使用的东西付费”的原则已经是足够好的答案了。如果允许使用静态运算符,除了更复杂的语法之外,你会得到什么(你必须在所有地方都写“X::operator+”,而不仅仅是“+”)?

92dk7w1h

92dk7w1h8#

基本上,类成员的静态运算符不会比非静态成员购买任何东西。
为类定义的任何运算符都必须至少采用该类类型的一个参数。
成员运算符采用隐式this参数形式的参数。
非成员运算符具有该类类型的显式参数。
操作员接口对操作员功能不关心;当我们调用a + b时,它负责生成代码,通过this参数或显式声明的参数传递a。因此,我们并不表示static与非static在如何使用该运算符方面的细微差别。
假设突然引入了一个要求,即最新的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成员运算符重写为非静态,删除最左边的参数,并(使用适当的词法卫生w.r.t. shadowing)将所有对该参数的引用替换为表达式*this(可以省略不必要的使用)。
这种转换非常简单,可以依赖程序员首先以这种方式编写代码。
static算子函数定义机制的功能较弱,例如它不能是virtual,而非静态算子函数定义机制可以是virtual

yzxexxkh

yzxexxkh9#

首先,我不知道为什么C中不允许使用静态运算符。作为一名Python程序员,我看到过一些使用@classmethod方法的API灵活性的很好的例子,这些方法被称为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;
}

所以,我认为你可以重载一些其他的操作符,使它在语法上看起来像是静态的。

相关问题