删除 "using namespace std::rel_ops" 可以改变行为吗?

4
我最近发现一个大型项目在全局作用域下的一个经常被引用的头文件中使用了"using namespace std::rel_ops;"。遗憾的是,这会导致问题,因为这两个函数模板声明变得模糊不清:
namespace std::rel_ops {
  template <class T>
  bool operator!=(const T&, const T&);
}
namespace std {
  template <class... TTypes, class... UTypes>
  constexpr bool operator!=(const tuple<TTypes...>&, const tuple<UTypes...>&);
}

因此,我尝试使用类似于 std::tie(a.m1, a.m2, a.m3) != std::tie(b.m1, b.m2, b.m3) 的表达式是无效的。

因此计划删除using namespace std::rel_ops;,然后修复由此产生的编译器错误,可能是通过定义更具体的比较操作符函数来实现的。但我还想评估一下,在不导致编译器错误的情况下,这种更改是否会改变项目中其他某个隐藏代码的含义。

在什么条件下,如果有的话,在带有指令using namespace std::rel_ops;和不带指令的两个C++程序之间可能有不同的行为,假设都没有必须的诊断?

我怀疑这需要另一个比std :: rel_ops 中的运算符函数模板不太特殊化的比较运算符函数模板,并且其有效行为与 std :: rel_ops 的定义不同。在一个真实的项目中,这两个条件似乎都很不可能同时存在。


由于ADL的存在,我认为在某些外部类型的特化情况下删除using namespace可能会导致各种奇怪的错误。 - bartop
@bartop 是的,但我认为移除指令只能缩小可行函数集合,而不能扩大,这限制了可能出错的范围。虽然我还没有进行更仔细的证明。 (我可能稍后会尝试,并查看是否可以自我回答,如果其他人没有给出好的答案。) - aschepler
在什么情况下,如果有的话,两个C++程序使用和不使用指令using namespace std::rel_ops;会在行为上有所不同,假设两者都没有必要的诊断错误?重载解析是一个可能的原因。如果using指令引入了更好的重载,则将使用该重载。删除该指令将删除该重载,这将改变调用哪个函数。 - NathanOliver
@NathanOliver 而且在 rel_ops 中,没有比任何潜在的声明更糟糕的重载了,对吧? - aschepler
1
@aschepler template <typename T, typename U> bool operator==(const T&, const U&) 可以吗? - L. F.
@L.F. 确实,这是更糟糕的匹配。 - bartop
2个回答

2

这里有一个例子,它大致讲述了这类错误的全部内容。根据using namespace的不同,程序会返回不同的值。


#include <utility>

class Type {
public:
    Type(int) {}
};

bool operator==(Type, Type) { return false; }

template<class T , class U>
bool operator!=(const T& lhs, const U& rhs) {
    return lhs == rhs;
}

using namespace std::rel_ops;

int main() {
    Type bar(1);
    Type baz(1);
    return bar != baz;
}

现场示例


rel_ops 运算符的签名为 template <class T> bool operator!=(const T&, const T&),因此这种情况(命名空间版本获胜的重载分辨率)不会发生。你能想到其他情况吗? - L. F.
是的,这看起来就像我怀疑的那样。感谢您确认它是可能的。(当然,在实际代码中定义operator!=operator==执行相同操作的任何类型非常不可能。) - aschepler

1
给定一对程序,其中一个使用了 using namespace std::rel_ops;,另一个没有违反需要诊断的规则,行为上的差异只能是由于在某些上下文中,rel_ops 的成员和另一个函数或函数模板之间的重载解析导致的,而这两个声明都是可行的。重载解析上下文可能是以下情况之一:
  • 二元比较表达式,如 E1 != E2
  • 显式调用,使用一个 operator-function-id 作为函数名,如 operator!=(E1, E2)
  • operator-function-id&operator-function-id 用作指向函数或引用函数的指针的初始化程序,在 [over.over]/1 中列出的上下文之一中,如 static_cast<bool(*)(const std::string&, const std::string&)>

实际上,另一个函数或函数模板特化比 std::rel_ops 的成员更差的情况并不难找到。例如:

另一个函数或模板专业化需要用户定义的转换。
class A {};
bool operator==(const A&, const A&);

class B {
public:
    B(A);
};
bool operator==(const B&, const B&);
bool operator!=(const B&, const B&);

void test1() {
    // With using-directive, selects std::rel_ops::operator!=<A>(const A&, const A&).
    // Without, selects operator!=(const B&, const B&).
    A{} != A{};
}

class C {
   operator int() const;
};
bool operator==(const C&, const C&);

void test2() {
    // With using-directive, selects std::rel_ops::operator!=<C>.
    // Without, selects the built-in != via converting both arguments to int.
    C{} != C{};

另一个函数或模板特化需要从派生类到基类的“转换”([over.best.ics]/6)。
class D {};
bool operator==(const D&, const D&);
bool operator!=(const D&, const D&);

class E : public D {};
bool operator==(const E&, const E&);

void test3() {
    // With using-directive, selects std::rel_ops::operator!=<E>.
    // Without, selects operator!=(const D&, const D&).
    E{} != E{};
}

另一个函数或模板特化具有右值引用参数类型。
class F {};
bool operator==(F&&, F&&);

void test4() {
    // With using-directive, selects std::rel_ops::operator!=<F>.
    // Without, selects operator!=(F&&, F&&).
    F{} != F{};
}

另一个函数是较不特定的函数模板的专门化。
namespace N1 {

class A{};
bool operator==(const A&, const A&);

template <typename T1, typename T2>
bool operator!=(const T1&, const T2&);

}

void test5() {
    // With using-directive, selects std::rel_ops::operator!=<N1::A>.
    // Without, selects N1::operator!=<N1::A,N1::A>.
    N1::A{} != N1::A{};
}

namespace N2 {

class B{};
bool operator==(const B&, const B&);

template <typename T>
bool operator!=(T&, T&);

}

void test6() {
    // With using-directive, selects std::rel_ops::operator!=<N2::B>.
    // Without, selects operator!=<const N2::B>.
    const N2::B b1;
    const N2::B b2;
    b1 != b2;
}

其他类别和示例是可能的,但这已经足以说明问题了。
就实际问题而言,在std::rel_ops中声明的任何比较运算符名称,如果相关的operator<或operator==也已定义,则不太可能实现为与同一类型的rel_ops定义非常不同的结果。如果"无效"或特殊值有特殊处理,例如对于浮点类型,当至少一个操作数是NaN值时,a<=b与!(b
(因此,对于最初的动机,我决定使用静态分析工具来查找现有代码中命名std::rel_ops成员的所有位置,以帮助检查编译时未捕获的意义意外更改。)

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