Может ли рефакторинг перегруженного оператора в функцию, не являющуюся членом, сломать какой-либо код?

Рассмотрим устаревший шаблон класса с перегруженными операторами сложения += и +.

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)

comment
Поломка, как в клиентском коде, не компилируется или как в ABI несовместимо?   -  person Oliver Charlesworth    schedule 29.09.2014
comment
Краткий ответ: Да, конечно, может. Но вряд ли...   -  person Mats Petersson    schedule 29.09.2014
comment
@MatsPetersson Я не смог привести конкретный пример   -  person TemplateRex    schedule 29.09.2014
comment
Обновлено @OliverCharlesworth, может ли это также нарушить совместимость с ABI?   -  person TemplateRex    schedule 29.09.2014
comment
Он может нарушить совместимость с ABI только в том случае, если он был экспортирован одним модулем и импортирован другим (нарушение динамической компоновки). Маловероятно, так как это была inline-функция, к тому же специализация шаблона. В сторону: ваша бесплатная функция действительно должна быть inline.   -  person Deduplicator    schedule 29.09.2014
comment
Я согласен с Deduplicator, я не могу придумать случай, когда он сломается, если мы не перекомпилируем что-то, а этого не произойдет (при условии действительного разрешения зависимостей в системе сборки) для встроенной в шаблон функции.   -  person Mats Petersson    schedule 29.09.2014
comment
@Deduplicator Я не смотрю на оптимизацию, inline здесь не требуется. В любом случае, ради аргумента, предположим, что весь код содержит только заголовок (обновлено в вопросе)   -  person TemplateRex    schedule 29.09.2014
comment
Насколько я вижу, будут нарушены только явные вызовы перегруженного оператора, и поломка происходит исключительно во время компиляции. За исключением крайне маловероятной возможности, о которой я упоминал выше, для которой, я думаю, вам нужны явные объявления extern-template-declarations и явные экземпляры в файле реализации. () Если это чистый заголовок, он сводится к времени атомной компиляции с поломкой API.   -  person Deduplicator    schedule 29.09.2014
comment
@Deduplicator inline является избыточным; неявно созданные функции шаблона имеют неявную встроенную связь.   -  person cdhowie    schedule 29.09.2014
comment
@cdhowie: Вы знаете, где это сказано? Потому что не нашел...   -  person Deduplicator    schedule 29.09.2014
comment
@Deduplicator В современном C/C++ inline в основном используется не для указания того, что функция должна быть встроена (стандарт не требует, чтобы компиляторы учитывали inline таким образом), а скорее для указания того, что ODR не применяется (функция разрешена быть определено в нескольких единицах перевода без ошибок). Поскольку неявные экземпляры шаблонов уже освобождены от ODR, inline на самом деле ничего не делает. Это не одно и то же, но они имеют одинаковый эффект (опять же, что касается неявных экземпляров).   -  person cdhowie    schedule 29.09.2014
comment
Что ж, очевидный ответ: да, это может привести к поломке во время компиляции, если вы где-нибудь явно сошлетесь на + как на функцию-член: X<Foo> z = x.operator+(y); Но это не связано с ADL, и также крайне маловероятно, что вы столкнетесь с этим.   -  person misberner    schedule 29.09.2014


Ответы (1)


Резюме

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

Здесь мы используем неявный конструктор внутри самого X<T>. Но даже если бы мы сделали этот конструктор explicit, пользователи могли бы добавить в пространство имен X<T> класс C<T>, содержащий неявное преобразование формы operator X<T>() const. Приведенные ниже сценарии будут продолжать действовать в этом случае.

Дружественная функция, не являющаяся членом, меньше всего ломается в том смысле, что она допускает неявные преобразования левого аргумента, которые не будут компилироваться для функции-члена шаблона класса. Шаблон функции, не являющейся членом, прерывает неявное преобразование правых аргументов.

Функция-член шаблона класса

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

Функция друга, не являющегося членом

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 */ }

В этом случае как левый, так и правый аргументы подвергаются выводу аргументов, и ни один из них не принимает во внимание неявные преобразования:

T t;
X<T> x;
x + t;  // ERROR, no implicit conversion on rhs
t + x;  // ERROR, no implicit conversion on lhs
person TemplateRex    schedule 29.09.2014