Операторы преобразования ссылочного типа: спрашивать о проблемах?

Когда я скомпилирую следующий код с помощью 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++ добавляет сложности к портированию кода.