Операторы преобразования ссылочного типа: спрашивать о проблемах?
Когда я скомпилирую следующий код с помощью g++
class A {};
void foo(A&) {}
int main()
{
foo(A());
return 0;
}
Появляются следующие сообщения об ошибках:
> g++ test.cpp -o test
test.cpp: In function ‘int main()’:
test.cpp:10: error: invalid initialization of non-const reference of type ‘A&’ from a temporary of type ‘A’
test.cpp:6: error: in passing argument 1 of ‘void foo(A&)’
После некоторого размышления эти ошибки имеют для меня большой смысл. A()
- это просто временное значение, а не назначаемое место в стеке, поэтому у него не будет адреса. Если у него нет адреса, я не могу ссылаться на него. Хорошо, отлично.
Но подождите! Если я добавлю следующий оператор преобразования в класс A
class A
{
public:
operator A&() { return *this; }
};
тогда все хорошо! Мой вопрос заключается в том, что это даже отдаленно безопасно. Что именно указывает this
, когда A()
создается как временное значение?
Мне внушают уверенность в том, что
void foo(const A&) {}
может принимать временные значения в соответствии с g++
и всеми другими компиляторами, которые я использовал. Ключевое слово const
всегда может быть отброшено, поэтому меня удивило бы, если бы существовали какие-либо фактические семантические различия между параметром const A&
и параметром A&
. Поэтому я предполагаю, что другой способ задать свой вопрос: почему ссылка const
на временное значение считается безопасным компилятором, тогда как ссылка не const
не является?
Ответы
Ответ 1
Дело не в том, что адрес не может быть принят (компилятор всегда может заказать его в стеке, что он делает с ref-to-const), это вопрос намерений программистов. С интерфейсом, который принимает A &, он говорит: "Я буду изменять то, что находится в этом параметре, чтобы вы могли читать после вызова функции". Если вы передадите ему временное, то после функции функция "изменена" не существует. Это (вероятно) ошибка программирования, поэтому она запрещена. Например, рассмотрим:
void plus_one(int & x) { ++x; }
int main() {
int x = 2;
float f = 10.0;
plus_one(x); plus_one(f);
cout << x << endl << f << endl;
}
Это не скомпилируется, но если временные объекты могут связываться с ref-to-non-const, они собираются, но имеют неожиданные результаты. В методе plus_one (f) f будет неявно преобразован во временный int, plus_one будет принимать темп и увеличивать его, оставляя лежащий в основе float f нетронутым. Когда plus_one вернется, это не повлияет. Это почти наверняка не то, что планировал программист.
Правило иногда беспорядок. Обычный пример (описанный здесь), пытается открыть файл, распечатать что-то и закрыть его. Вы хотели бы иметь возможность:
ofstream("bar.t") << "flah";
Но вы не можете, потому что оператор < < принимает ref-to-non-const. Ваши параметры разбивают его на две строки или вызывают метод, возвращающий ref-to-non-const:
ofstream("bar.t").flush() << "flah";
Ответ 2
Когда вы назначаете значение r для ссылки const, вам гарантируется, что временное не будет уничтожено до тех пор, пока ссылка не будет уничтожена. Когда вы назначаете неконстантную ссылку, такая гарантия не предоставляется.
int main()
{
const A& a2= A(); // this is fine, and the temporary will last until the end of the current scope.
A& a1 = A(); // You can't do this.
}
Вы не можете безопасно отбросить константу волей-неволей и ожидать, что все будет работать. Существуют разные семантики для константных и неконстантных ссылок.
Ответ 3
В результате могут возникнуть некоторые проблемы: компилятор MSVC (компилятор Visual Studio, проверенный с помощью Visual Studio 2008) скомпилирует этот код без проблем. Мы использовали эту парадигму в проекте для функций, которые обычно принимали один аргумент (кусок данных для переваривания), но иногда хотели искать фрагмент и возвращать результаты обратно вызывающему. Другой режим был включен с помощью трех аргументов: вторым аргументом была информация для поиска по умолчанию (по умолчанию ссылка на пустую строку), а третий аргумент - для возвращаемых данных (по умолчанию ссылка на пустой список желаемого типа).
Эта парадигма работала в Visual Studio 2005 и 2008, и нам пришлось реорганизовать ее так, чтобы список был построен и возвращен вместо того, чтобы скомпилировать с g++.
Если есть способ установить переключатели компилятора либо запретить подобное поведение в MSVC, либо разрешить его в g++, я был бы рад узнать; разрешимость компилятора/ограничителя MSVC компилятора g++ добавляет сложности к портированию кода.