【问题标题】:Could removing "using namespace std::rel_ops" change behavior?删除“使用命名空间 std::rel_ops”可以改变行为吗?
【发布时间】:2020-03-25 13:53:54
【问题描述】:

我最近发现一个大型项目在经常包含的头文件和全局命名空间中有一个“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 是的,但我认为删除指令只能缩小而不是扩大可行的功能集,这限制了可能出错的地方。不过,我还没有经过更仔细的证明。 (我可能会稍后尝试,看看我是否可以自我回答,如果其他人没有先给出好的答案。)
  • 如果有的话,两个 C++ 程序在什么情况下可以使用命名空间 std::rel_ops;考虑到两者都不是符合要求的诊断格式,因此行为不同? 想到了重载解决方案。如果 using 指令带来更好的重载,则将使用该指令。删除指令将删除重载,这将改变调用哪个函数。
  • @NathanOliver 而且没有多少潜在的声明可能比rel_ops 中的任何东西都更糟糕,对吧?
  • @aschepler template &lt;typename T, typename U&gt; bool operator==(const T&amp;, const U&amp;) 也许?

标签: c++ language-lawyer comparison-operators


【解决方案1】:

你有一个这样的例子,它更详细地说明了这类错误。根据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;
}

Live example

【讨论】:

  • rel_ops 运算符具有签名template &lt;class T&gt; bool operator!=(const T&amp;, const T&amp;),因此这种情况(命名空间版本赢得重载决议)不会发生。你能想到其他情况吗?
  • 是的,这看起来像我怀疑的情况。感谢您确认这是可能的。 (当然,在实际代码中,将operator!= 定义为与operator== 相同的任何类型都不太可能。)
【解决方案2】:

给定带有和不带有 using namespace std::rel_ops; 的一对程序,它们不违反需要诊断的规则,行为上的差异只能是由于 rel_ops 的成员与另一个函数或函数模板之间的重载解决方案在某些情况下,这两个声明都是可行的,这是一个更糟糕的过载。重载决议上下文可能是:

  • 二进制比较表达式,如E1 != E2
  • 使用 operator-function-id 作为函数名的显式调用,例如 operator!=(E1, E2)
  • 使用 operator-function-id&amp;operator-function-id 作为函数指针或函数引用的初始值设定项,在其中之一[over.over]/1 中列出的上下文,例如 static_cast&lt;bool(*)(const std::string&amp;, const std::string&amp;)&gt;

实际上,另一个函数或函数模板特化比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;
}

其他类别和示例也是可能的,但这并不重要。

就实际问题而言,考虑到相关的 operator&lt; 或 @还定义了 987654337@。如果“无效”或特殊值有特殊处理可能会有所不同,例如当至少一个操作数是 NaN 值时,浮点类型 a &lt;= b 不等于 !(b &lt; a)。但是涉及到不同类型的隐式转换的情况很容易导致不同的行为。在派生到基础的转换之后,派生数据成员中的任何信息都将很可能被忽略。在转换构造函数或转换函数之后,比较的是完全不同类型的值,这些值可能以某种方式代表原始参数,但可能不代表它们的完整功能身份。

(因此,出于最初的动机,我认为值得使用静态分析工具来查找现有代码中命名std::rel_ops 成员的所有位置,以帮助检查仅编译未捕获到的意料之外的变化。 )

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-02-27
    • 2011-07-14
    • 2016-03-02
    • 2013-10-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多