Может ли удаление "использование пространства имен std :: rel_ops" изменить поведение? - PullRequest
3 голосов
/ 25 марта 2020

Недавно я обнаружил, что у большого проекта есть «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 функций оператора сравнения. Но я также хочу, чтобы go оценил, возможно ли, чтобы это изменение могло изменить значение некоторого кода, спрятанного где-то еще в этом большом проекте, без возникновения ошибки компилятора.

В каких условиях если таковые имеются, могут ли две программы на С ++ с директивой using namespace std::rel_ops; и без нее отличаться по поведению, учитывая, что ни одна из них не имеет неправильной формы с требуемым диагнозом c?

Я подозреваю, что для этого потребуется другая функция оператора сравнения шаблон, который меньше специализирован, чем один в std::rel_ops, и который имеет эффективное поведение, отличное от определения std::rel_ops. Оба условия кажутся маловероятными в реальном проекте, и даже менее вероятно, если рассматривать их вместе.

Ответы [ 2 ]

2 голосов
/ 25 марта 2020

Там у вас есть один пример этого, который более всего рассказывает об этом классе ошибок. В зависимости от 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;
}

Живой пример

1 голос
/ 29 марта 2020

При наличии пары программ с using namespace std::rel_ops; и без него, которые не нарушают правило, требующее диагностики c, различие в поведении может быть связано только с разрешением перегрузки между элементом rel_ops и другой функцией или шаблон функции, который в некотором контексте является перегрузкой хуже с обоими объявлениями жизнеспособными. Контекст разрешения перегрузки может быть следующим:

  • двоичное выражение сравнения, например E1 != E2
  • явный вызов с использованием operator-function-id в качестве имени функции Например, operator!=(E1, E2)
  • использование идентификатор-оператора-функции или & идентификатор-оператора-функции в качестве инициализатора для указателя на функцию или ссылку функционировать в одном из контекстов, перечисленных в [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{};

Другая специализация функции или шаблона требует "преобразования в производную базу" (* 1031) * [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{};
}

Другая специализация функции или шаблона имеет тип справочного параметра rvalue.

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==. Это может иметь значение, если «недопустимые» или специальные значения имеют специальную обработку, например, как для типов с плавающей запятой a <= b не эквивалентно !(b < a), когда хотя бы один операнд является значением NaN. Но случаи, включающие неявное преобразование в другой тип, могут довольно легко привести к другому поведению. После преобразования в производную базу любая информация в элементах производных данных, скорее всего, будет игнорироваться. После конструктора преобразования или функции преобразования сравниваются значения совершенно другого типа, которые предположительно каким-то образом представляют исходные аргументы, но могут не представлять их полную функциональную идентичность.

(Поэтому для исходной мотивации я решил Стоит использовать инструмент анализа stati c, чтобы найти все места в существующем коде с именем члена std::rel_ops, чтобы помочь проверить непреднамеренные изменения в значении, которые не обнаруживаются при простой компиляции.)

...