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

Could removing "using namespace std::rel_ops" change behavior?

我最近发现一个大型项目在经常包含的头文件和全局命名空间中有一个“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;,然后修复由此导致的编译器错误,可能是通过定义更具体的比较运算符函数。但我还想通过评估此更改是否可能改变隐藏在这个大型项目其他地方的某些代码的含义,而不会导致编译器错误的练习。

在什么条件下,如果有的话,两个 C++ 程序有和没有指令 using namespace std::rel_ops; 的行为可能不同,因为两者都没有错误的诊断要求?

我怀疑它需要另一个比较运算符函数模板,它比 std::rel_ops 中的一个 更少 专门化,并且具有与 [=16= 不同的有效行为] 定义。这两种情况在实际项目中似乎都不太可能,更不可能一起考虑。

你有一个这样的例子,它更详细地说明了这个 class 的错误。根据 using namespace 程序 returns 不同的值。


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

Live example

给定有和没有 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{};

其他函数或模板特化需要派生到基础 "Conversion" ([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 中声明的任何比较运算符名称都不太可能实现以给出与同一类型的 rel_ops 定义截然不同的结果,因为还定义了相关的 operator<operator==。如果 "invalid" 或特殊值有特殊处理,可能会有所不同,例如当至少一个操作数是 NaN 值时,浮点类型 a <= b 不等同于 !(b < a)。但是涉及到不同类型的隐式转换的情况很容易导致不同的行为。在派生到基础的转换之后,派生数据成员中的任何信息很可能会被忽略。在转换构造函数或转换函数之后,比较是在完全不同类型的值上进行的,这些值可能以某种方式代表原始参数,但可能不代表它们的完整功能标识。

(因此,出于最初的动机,我决定值得使用静态分析工具来查找现有代码中所有命名 std::rel_ops 成员的地方,以帮助检查未捕获的意想不到的变化通过编译。)