Проблема с преобразованием к самому себе в GCC
Не так давно я прикрутил фичу, описанную в предыдущем посте, к своим умным указателям, и вот недавно счастье было разрушено падением программы. Проблема была локализована в следующем коде :
const HandlePointer<lib3dsfile>& file = loadFile(); |
Функция loadFile() возвращала HandlePointer<Lib3dsFile> по значению, и по идее это значение должно было быть привязано (bind) к константной ссылке. Время жизни временного (temporary) объекта в этом случае по Стандарту должно быть продлено и временный объект должен быть жив до тех пор, пока жива константная ссылка. Однако, добавив диагностических логов, я с удивлением отметил, что деструктор временного объекта в данном случае вызывается сразу после выполнения этой строки, не дожидаясь выхода ссылки из области видимости. Замена ссылки на объект-копию магическим образом исцеляло программу, временный объект уничтожался после вызова конструктора копий, и программа работала как раньше.
Добавив еще чуть больше логов, я удивился еще раз, увидев, что в указанном выражении вызывается
оператор преобразования HandlePointer<T>::operator T& () , то есть объект преобразовывался к самому себе, однако при этом объект уже не привязывался к константной ссылке, а уничтожался автоматически после вычисления полного выражения (full expression).
Дома для проверки написал тестовую программу, которая объявляла класс Test и оператор преобразования к типам Test& и const Test& :
class Test { public: Test() { printf("Default constructor\r\n"); } ~Test() { printf("Destructor\r\n"); } Test(const Test& copy) { printf("Copy constructor\r\n"); } Test& operator= (const Test& copy) { printf("operator =\r\n"); return *this; } // operator Test& () { printf("operator Test&\r\n"); return *this; } operator const Test& () const { printf("operator const Test&\r\n"); return *this; } } Test someFunc() { return Test(); } int main() { { printf("Begin\r\n"); const Test& test = someFunc(); printf("End\r\n"); } } |
Натравив на него компиляторы Visual C++ и GCC, посмотрел на результаты. Произошло все так же, как и в случае шаблонных классов :
Вывод программы, скомпилированной Visual C++ :
Begin
Default constructor
End
Destructor
Вывод программы, скомпилированной GCC :
Begin
Default constructor
operator Test&
Copy constructor
Destructor
operator Test&
Destructor
End
На обилие конструкторов копий и деструкторов в выводе GCC можно не обращать внимания – это
всего лишь говорит о том, что GCC не выполнил здесь RVO (Return Values Optimization). Главное –
что в Visual C++ временный объект живет до конца блока { }, а в GCC уничтожается сразу.
Почему поведение GCC здесь мне представляется неверным ? Допустим, у вас есть класс. У класса есть
дефолтные операторы преобразования их к ссылке и к константной ссылке, генерируемые компилятором автоматически. И временные объекты успешно привязываются к константным ссылкам, продлевая их время жизни. Однако, если вы вдруг (как сейчас я) захотите написать свою версию такого оператора (что в принципе, бессмысленно, но в контексте использования шаблонов смысл приобретает), то вы уже не сможете действовать по старым правилам. Таким образом, явное определение пользовательского оператора преобразования (такого же, как и дефолтный, по сути) изменяет поведение объектов и семантику в целом.
В общем, сбило порядочно меня все это с толку, и я обратился за помощью на блог Алены С++ (в котором как раз недавно читал про RVO и привязку временных объектов к const& ) и на RSDN.
На следующий день поступили комментарии, позволяющие разобраться в том, что же все-таки происходит и какой из компиляторов в данном случае прав.
Краткое резюме беседы (для тех, кому лениво читать обсуждение) :
Здесь мы видим следствие изначального внутреннего противоречия в Стандарте 98 (см ветку на RSDN).
Стандарт был скорректирован в дальнейшем в ходе внесения правок и уточнений.
На данный момент корректным поведением является поведение, при котором компилятор не должен вызвать переопределенные операторы преобразования в случае, если это преобразование выполняется к ссылке на тот же тип или к объекту того же типа:
12.3.2/1
…
A conversion function is never used to convert a (possibly cv-qualified)
object to the (possibly cv-qualified) same object type (or a reference to it)
Таким образом, компилятор GCC на данный момент не соответствует исправленному Стандарту.
Что ж, печально, придется использовать вместо неявных преобразований явный вызов функции.
Хотя, возможно, есть способ, который бы позволил ограничить шаблонный метод преобразования таким образом, чтобы он не использовался, когда тип T идентичен типу U ? Я попробовал пошаманить с
шаблонами и SFINAE, но пока ничего путного не вышло. Если у кого-нибудь есть такое решение,
было бы интересно о нем узнать.
1