将重载运算符重构为非成员函数会破坏任何代码吗?

9
考虑一个拥有重载加法运算符+=+的传统类模板。
template<class T>
class X
{
public:
    X() = default;
    /* implicict */ X(T v): val(v) {}

    X<T>& operator+=(X<T> const& rhs)       { val += rhs.val; return *this; }
    X<T>  operator+ (X<T> const& rhs) const { return X<T>(*this) += rhs;    } 

private:
    T val;
};

在代码审查中,观察到+可以用+=实现,那么为什么不将其作为非成员函数(并保证左右参数对称性)?
template<class T>
class X
{
public:
    X() = default;
    /* implicit */ X(T v): val(v) {}

    X<T>& operator+=(X<T> const& rhs)       { val += rhs.val; return *this; }

private:
    T val;
}; 

template<class T>
X<T> operator+(X<T> const& lhs, X<T> const& rhs)
{ 
    return X<T>(lhs) += rhs; 
}

看起来很安全,因为所有使用++=的有效表达式都保留了它们的原始语义。

问题:将operator+从成员函数重构为非成员函数会破坏任何代码吗?

破坏的定义(由差到好)

  • 新代码将编译,而旧代码在旧情况下无法编译
  • 旧代码将无法编译,而在旧情况下可以编译
  • 新代码将悄悄调用不同的operator+(从基类或通过ADL引入的相关命名空间)

“Breakage” 的意思是“客户端代码无法编译”,或者是“ABI 不兼容”? - Oliver Charlesworth
@MatsPetersson 我还没有想出一个具体的例子。 - TemplateRex
@cdhowie:你知道在哪里说了吗?因为我找不到它... - Deduplicator
1
在现代的C/C++中,inline主要用于指示函数不受ODR(One Definition Rule)的约束(允许在多个翻译单元中定义函数而不会出错),而不是表示该函数应该被内联(标准并不要求编译器遵守inline)。由于模板的隐式实例化已经免除了ODR的限制,因此inline实际上并没有什么作用。它们不是同一件事,但它们具有相同的效果(再次强调,这只针对隐式实例化)。 - cdhowie
1
显而易见的答案是可以的,如果你在某个地方明确地将 + 作为成员函数引用:X<Foo> z = x.operator+(y);,那么它可能会在编译时破坏一些东西。但这与 ADL 无关,并且极不可能遇到这种情况。 - misberner
显示剩余7条评论
1个回答

3

摘要

答案是,会有始终存在的破碎。关键是函数模板参数推导不考虑隐式转换。我们考虑三种情况,涵盖重载运算符可以采取的三种语法形式。

在这里,我们在X<T>内部使用一个隐式构造函数。但即使我们将该构造函数设置为explicit,用户仍然可以向X<T>的命名空间添加包含形如operator X<T>() const的隐式转换的C<T>类。在这种情况下,下面的场景将继续保持。

非成员友元函数在这个意义上最少会中断,在这个意义上它将允许lhs参数隐式转换,而这对于类模板的成员函数来说无法编译。非成员函数模板在rhs参数上打破了隐式转换。

类模板的成员函数

template<class T>
class X
{
public:
    /* implicit */ X(T val) { /* bla */ }
//...
    X<T> operator+(X<T> const& rhs) { /* bla */ }
//...
};

这段代码将允许表达式的使用。
T t;
X<T> x;
x + t;  // OK, implicit conversion on non-deduced rhs
t + x;  // ERROR, no implicit conversion on deduced this pointer

Non-member friend function

template<class T>
class X
{
public:
    /* implicit */ X(T val) { /* bla */ }
//...
    friend 
    X<T> operator+(X<T> const& lhs, X<T> const& rhs) { /* bla */ }
//...
};

friend函数不是模板,因此不进行参数推导,左操作数和右操作数都考虑隐式转换。

T t;
X<T> x;
x + t;  // OK, implicit conversion on rhs
t + x;  // OK, implicit conversion on lhs

非成员函数模板

template<class T>
class X
{
public:
    /* implicit */ X(T val) { /* bla */ }
//...
};

template<class T> 
X<T> operator+(X<T> const& lhs, X<T> const& rhs) { /* bla */ }

在这种情况下,lhs和rhs参数都会被推导出来,而且都不考虑隐式转换:
T t;
X<T> x;
x + t;  // ERROR, no implicit conversion on rhs
t + x;  // ERROR, no implicit conversion on lhs

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