46

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

struct X
{
   void operator+(X);
}

或在结构/类之外,例如:

void operator+(X, X);

但不是:

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

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

PS 第一个和第二个定义同时存在会产生歧义:

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

9 回答 9

18

我没有任何关于这个概念的 C++ 讨论的具体知识,所以请随意忽略这一点。

但对我来说,你的问题倒过来了。问题应该是,“为什么允许这种语法?”

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

它不会更容易实现非对称运算符(即:)operator+(const X &x, const Y &y)。如果您需要私有访问来实现这一点,您仍然需要在其中一个类中为它们声明一个朋友声明。

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


或者,换一种说法:

自由函数可以做静态函数系统可以做的一切,甚至更多

通过使用自由函数,您可以获得模板中使用的运算符的依赖于参数的查找。您不能使用静态函数来做到这一点,因为它们必须是特定类的成员。并且您不能从类外部添加到类,而可以添加到命名空间。因此,如果您需要将运算符放在特定的命名空间中以使某些 ADL 代码工作,您可以。您不能使用静态函数运算符来做到这一点。

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


哪个可以在不实例化它们的情况下使用函子?

这在术语上是矛盾的。“函子”是“函数对象”。类型不是对象;因此,它不能是函子。它可以是一种在实例化时会产生仿函数的类型。但单独的类型不会是函子。

此外,能够声明Typename::operator()静态并不意味着它Typename()会做你想做的事。该语法已经具有实际意义:Typename通过调用默认构造函数来实例化一个临时对象。

最后,即使一切都不是这样,那又有什么好处呢?大多数采用某种类型的可调用对象的模板函数与函数指针和仿函数一样工作。你为什么要限制你的接口,不仅仅是函子,而是不能有内部数据的函子?这意味着您将无法通过捕获 lambda 等。

一个不可能包含状态的函子有什么用?为什么要强制用户传递没有状态的“函子”?为什么要阻止用户使用 lambda?

所以你的问题源于一个错误的假设:即使我们有它,它也不会给你你想要的。

于 2012-08-10T01:28:17.073 回答
18

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

X x1;
X x2;

现在,让我们假设我们正在使用普通的成员函数而不是运算符 - 假设我在您的示例中更改operator+为。plus

这三个调用中的每一个看起来像:

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

现在,当使用编译器进行运算符调用时+,编译器如何知道在 ? 范围内查找您的运算符X?对于普通的静态成员函数,它不能做到这一点,并且运算符没有得到特殊的消除歧义。

现在考虑是否在程序中同时声明了第二种和第三种形式。如果你说x1 + x2编译器要么必须总是选择自由函数,要么调用会模棱两可。唯一真正的选择是x1 X::+ x2看起来很丑的东西。鉴于这一切,我确信标准委员会决定简单地禁止静态成员版本,因为它可以完成的任何事情都可以通过免费朋友功能来完成。

于 2012-08-10T02:36:25.053 回答
2

静态成员函数可用于帮助类但由于某种原因不是成员的实用程序。很容易想象,在表示为静态类成员函数的实用程序中,拥有运算符可能很有用。

当然,如果某些重载运算符将类 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或 awidget::compare并且可以有一个squiggle::comparethat widgets

因此,C++ 中唯一不支持的方面是使用运算符的语法糖化。

也许它不是一个足够有用的想法来保证支持(到目前为止!)毕竟,这不是允许对 C++ 程序进行革命性重组的东西。但它会修复语言的不完整性。

另外,请考虑运算符的类重载new并且delete是隐式静态的!所以不完备性已经有了一点例外。

于 2013-04-12T22:31:00.047 回答
2

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

于 2016-03-04T14:52:41.567 回答
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);
    }
};

所以......我明白为什么其他答案可能对此持谨慎态度......但我完全认为在可能的情况下使用静态运算符重载是有好处的。我已经看到过去出现了一些关于添加静态运算符重载的各种努力的建议(例如,这个关于静态 operator() 的建议)。也许这值得进行更广泛的讨论,甚至是提议。我当然遇到过很多情况,我认为拥有这个功能会更清晰。命名很难,而且是一个被低估的问题。如果我看到一个带有 form 的函数op_shl(auto a, auto b),我会比看到一个 form 的函数更不确定如何处理这个函数:operator<<(auto a, auto b). 我们可以就编写好的评论和遵循软件设计的最佳实践提出我们想要的所有论点,但归根结底,静态运算符语法没有歧义。

于 2021-10-04T13:55:09.657 回答
0

这可能是原因。

因为每个人都operator需要一个或多个operands。因此,如果我们将其声明为,static那么我们就不能使用对象(操作数)来调用它。

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

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

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

因此,假设我们将运算符重载函数声明为静态。那么上述条件中的第一个将不被满足。

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

所以运算符重载函数必须是non-static member function.

但有一个例外。

如果我们使用友元函数进行运算符重载,那么它可以声明为静态的。

于 2012-08-10T03:06:40.403 回答
0

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

于 2013-07-31T13:26:42.217 回答
0

基本上,类成员静态运算符不会比非静态成员购买任何东西。

为类定义的任何运算符都必须至少采用该类类型的一个参数。

成员运算符以隐式参数的形式获取该this参数。

非成员运算符具有该类类型的显式参数。

操作员功能的操作员界面无关紧要;当我们调用 时a + b,它负责生成代码以通过a参数this或作为显式声明的参数传递。因此,关于如何使用运算符,我们并没有表达静态与非静态的细微差别。

假设突然引入了最新的 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为非静态,删除最左边的参数,并且(使用适当的词法卫生 wrt 阴影)用表达式替换对该参数的所有引用*this(可以省略不必要的使用)。

这种转换非常简单,可以依靠程序员首先以这种方式编写代码。

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

于 2018-03-22T00:11:13.730 回答
0

首先,我不知道为什么 C++ 中不允许使用静态运算符。作为一名 Python 程序员,我已经看到了一些使用 @classmethod 方法的 API 灵活性很好的例子,这些方法被称为 Class.method,似乎没有人因这种事情的存在而受苦。

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

现在好了,即使你不能合法地这样做,你也可以使用#define's 和一些运气作弊) 免责声明!: 也许你不应该在家里这样做,但这取决于你


#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;
}


因此,我认为您可能会以这种方式重载其他一些运算符,并使其在语法上看起来好像它们是静态的。

于 2020-05-06T12:11:07.177 回答