Почему незаконно использовать адрес rvalue временным?

В соответствии с "Как обойти предупреждение "rvalue, используемое как lvalue" ?, Visual Studio будет просто предупреждать о коде, например:

int bar() {
   return 3;
}

void foo(int* ptr) {

}

int main() {
   foo(&bar());
}

В С++ не разрешается принимать адрес временного (или, по крайней мере, объекта, на который ссылается выражение rvalue?), и я думал, что это связано с тем, что временным ресурсам не гарантируется даже хранение.

Но тогда, хотя диагностика может быть представлена ​​в любой форме, которую выбирает компилятор, я бы все же ожидал, что MSVS будет ошибкой, а не предупреждением в таком случае.

Итак, гарантированы ли временные хранилища? И если да, то почему приведенный выше код не разрешен в первую очередь?

Ответы

Ответ 1

Вы правы, говоря, что "временным ресурсам не гарантируется даже хранение" в том смысле, что временное не может быть сохранено в адресной памяти. Фактически, очень часто функции, скомпилированные для архитектур RISC (например, ARM), возвращают значения в регистры общего использования и будут ожидать ввода в этих регистрах.

MSVS, создающий код для x86-архитектур, всегда может создавать функции, возвращающие их значения в стеке. Поэтому они сохраняются в адресной памяти и имеют действительный адрес.

Ответ 2

Собственно, в оригинальном дизайне языка было разрешено использовать адрес временного. Как вы заметили правильно, нет никаких технических причин не допускать этого, и MSVC по-прежнему позволяет это через нестандартное языковое расширение.

Причина, по которой С++ сделала это незаконным, заключается в том, что привязка ссылок на временные конфликты с другой функцией языка С++, которая была унаследована от преобразования C: Implicit type. Рассмотрим:

void CalculateStuff(long& out_param) {
    long result;
    // [...] complicated calculations
    out_param = result;
}

int stuff;
CalculateStuff(stuff);  //< this won't compile in ISO C++

CalculateStuff() должен возвращать свой результат через выходной параметр. Но на самом деле происходит следующее: функция принимает long&, но ей присваивается аргумент типа int. Через C неявное преобразование типа, что int теперь неявно преобразуется в переменную типа long, создавая неназванное временное в этом процессе. Таким образом, вместо переменной stuff функция действительно работает с неназванным временным, и все побочные эффекты, применяемые этой функцией, будут потеряны после того, как это временно будет уничтожено. Значение переменной stuff никогда не изменяется.

Были введены ссылки на С++, чтобы разрешить перегрузку оператора, поскольку с точки зрения вызывающего лица они синтаксически идентичны вызовам по-значению (в отличие от вызовов указателей, для которых требуется явная & на стороне вызывающего абонента). К сожалению, именно синтаксическая эквивалентность приводит к проблемам при объединении с неявным преобразованием типа C.

Так как Stroustrup хотел сохранить обе функции (ссылки и C-совместимость), он представил правило, которое все мы знаем сегодня: Безымянные временные привязки привязываются только к ссылкам const. С помощью этого дополнительного правила вышеприведенный образец больше не компилируется. Поскольку проблема возникает только тогда, когда функция применяет побочные эффекты к эталонному параметру, по-прежнему безопасно связывать неназванные временные ссылки с константными ссылками, что, тем не менее, разрешено.

Вся эта история также описана в главе 3.7 Проекта и Эволюции С++:

Причина того, что ссылки должны быть инициализированы с помощью не-lvalues, заключалась в том, чтобы позволить различие между вызовом по значению и вызовом по ссылке быть деталью, указанной вызываемой функцией, и не представляет интереса для вызывающего. Для ссылок const это возможно; для non-const ссылок это не так. Для версии 2.0 определение С++ было изменено, чтобы отразить это.

Я также смутно помню, как читал в газете, который впервые обнаружил это поведение, но сейчас я не могу вспомнить. Может, кто-то может мне помочь?

Ответ 3

Конечно, временные хранилища хранятся. Вы можете сделать что-то вроде этого:

template<typename T>
const T *get_temporary_address(const T &x) {
    return &x;
}

int bar() { return 42; }

int main() {
    std::cout << (const void *)get_temporary_address(bar()) << std::endl;
}

В С++ 11 вы можете сделать это с помощью ссылок, не содержащих константы:

template<typename T>
T *get_temporary_address(T &&x) {
    return &x;
}

int bar() { return 42; }

int main() {
    std::cout << (const void *)get_temporary_address(bar()) << std::endl;
}

Обратите внимание, конечно, что разыменование указателя, о котором идет речь (вне get_temporary_address), является очень плохой идеей; временное только доживает до конца полного выражения, и поэтому, имея указатель на него, это выражение почти всегда является рецептом для катастрофы.

Кроме того, обратите внимание, что компилятор никогда не должен отклонять недействительную программу. Стандарты C и С++ просто требуют диагностики (например, ошибки или предупреждения), по которым компилятор может отклонить программу или может скомпилировать программу с поведением undefined во время выполнения. Если вы хотите, чтобы ваш компилятор строго отклонил программы, которые производят диагностику, настройте его для преобразования предупреждений в ошибки.

Ответ 4

Временные объекты имеют память. Иногда компилятор также создает временные. В тех случаях, когда эти предметы собираются уйти, т.е. Они не должны собирать важные изменения случайно. Таким образом, вы можете получить временное значение только с помощью ссылки rvalue или ссылки на константу, но не с помощью неконстантной ссылки. Принимая адрес объекта, который собирается уйти, также чувствует себя как опасная вещь и, следовательно, не поддерживается.

Если вы уверены, что вам действительно нужна неконстантная ссылка или указатель от временного объекта, вы можете вернуть ее из соответствующей функции-члена: вы можете вызывать неконстантные функции-члены во временных рядах. И вы можете вернуть this из этого члена. Однако обратите внимание, что система типов пытается вам помочь. Когда вы обманываете его, вы лучше знаете, что то, что вы делаете, - это правильная вещь.

Ответ 5

Как уже упоминалось, мы все договорились о временном хранении.

почему незаконно брать адрес временного?

Поскольку временные ресурсы выделены в стеке, компилятор может использовать этот адрес для любых других целей, которые он хочет.

int foo()
{
int myvar=5;
return &myvar;
}

int main()
{
int *p=foo();
print("%d", *p);
return 0;
}

Скажем, адрес "myvar" равен 0x1000. Эта программа, скорее всего, будет печатать 99, даже если она незаконна для доступа к 0x1000 в main(). Хотя, не обязательно все время.

С небольшим изменением на вышеуказанный main():

int foo()
{
int myvar=5;
return &myvar; // address of myvar is 0x1000
}

int main()
{
int *p=foo(); //illegal to access 0x1000 here
print("%d", *p);
fun(p); // passing *that address* to fun()
return 0;
}

void fun(int *q) 
{
 int a,b; //some variables
 print("%d", *q);
}

Второй printf вряд ли напечатает "5", поскольку компилятор мог бы даже выделить ту же часть стека (которая содержит 0x1000) для fun(). Независимо от того, печатает ли он "5" для обоих printfs ИЛИ в любом из них, это чисто непреднамеренное побочное влияние на то, как используется/распределяется память стека. Вот почему это незаконно для доступа к адресу, который не является живым в области.

Ответ 6

У Temporaries есть хранилище. Они выделяются в стек вызывающего абонента (примечание: может быть предметом соглашения о вызове, но я думаю, что все они используют стек вызывающего):

caller()
{
 callee1( Tmp() );
 callee2( Tmp() );
}

Компилятор выделяет пространство для результата Tmp() в стеке caller. Вы можете взять адрес этой ячейки памяти - это будет некоторый адрес в стеке caller. Какой компилятор не гарантирует, что он сохранит значения в этом адресе стека после возврата callee. Например, компилятор может разместить там еще один временный и т.д.

РЕДАКТИРОВАТЬ: Я считаю, что он запретил исключать такой код:

T bar();
T * ptr = &bar();

потому что это очень вероятно приведет к проблемам.

EDIT: вот маленький тест:

#include <iostream>

typedef long long int T64;

T64 ** foo( T64 * fA )
{

 std::cout << "Address of tmp inside callee : " << &fA << std::endl;

 return ( &fA );
}

int main( void )
{
 T64 lA = -1;
 T64 lB = -2;
 T64 lC = -3;
 T64 lD = -4;

T64 ** ptr_tmp = foo( &lA );
 std::cout << "**ptr_tmp = *(*ptr_tmp ) = lA\t\t\t\t**" << ptr_tmp << " = *(" << *ptr_tmp << ") = " << **ptr_tmp << " = " << lA << std::endl << std::endl;

 foo( &lB );
 std::cout << "**ptr_tmp = *(*ptr_tmp ) = lB (compiler override)\t**" << ptr_tmp << " = *(" << *ptr_tmp << ") = " << **ptr_tmp << " = " << lB << std::endl
   << std::endl;

 *ptr_tmp = &lC;
 std::cout << "Manual override" << std::endl << "**ptr_tmp = *(*ptr_tmp ) = lC (manual override)\t\t**" << ptr_tmp << " = *(" << *ptr_tmp << ") = " << **ptr_tmp
   << " = " << lC << std::endl << std::endl;

 *ptr_tmp = &lD;
 std::cout << "Another attempt to manually override" << std::endl;
 std::cout << "**ptr_tmp = *(*ptr_tmp ) = lD (manual override)\t\t**" << ptr_tmp << " = *(" << *ptr_tmp << ") = " << **ptr_tmp << " = " << lD << std::endl
   << std::endl;

 return ( 0 );
}

Выход программы GCC:

Address of tmp inside callee : 0xbfe172f0
**ptr_tmp = *(*ptr_tmp ) = lA               **0xbfe172f0 = *(0xbfe17328) = -1 = -1

Address of tmp inside callee : 0xbfe172f0
**ptr_tmp = *(*ptr_tmp ) = lB (compiler override)   **0xbfe172f0 = *(0xbfe17320) = -2 = -2

Manual override
**ptr_tmp = *(*ptr_tmp ) = lC (manual override)     **0xbfe172f0 = *(0xbfe17318) = -3 = -3

Another attempt to manually override
**ptr_tmp = *(*ptr_tmp ) = lD (manual override)     **0xbfe172f0 = *(0x804a3a0) = -5221865215862754004 = -4

Программный вывод VС++:

Address of tmp inside callee :  00000000001EFC10
**ptr_tmp = *(*ptr_tmp ) = lA                           **00000000001EFC10 = *(000000013F42CB10) = -1 = -1

Address of tmp inside callee :  00000000001EFC10
**ptr_tmp = *(*ptr_tmp ) = lB (compiler override)       **00000000001EFC10 = *(000000013F42CB10) = -2 = -2

Manual override
**ptr_tmp = *(*ptr_tmp ) = lC (manual override)         **00000000001EFC10 = *(000000013F42CB10) = -3 = -3

Another attempt to manually override
**ptr_tmp = *(*ptr_tmp ) = lD (manual override)         **00000000001EFC10 = *(000000013F42CB10) = 5356268064 = -4

Обратите внимание, что как GCC, так и VС++ резервируют в стеке main скрытые локальные переменные для временных файлов и MIGHT молча используют их повторно. Все идет нормально, пока последнее ручное переопределение: после последнего ручного переопределения мы имеем дополнительный отдельный вызов std::cout. Он использует пространство стека, где мы только что что-то написали, и в результате получаем мусор.

Нижняя строка: как GCC, так и VС++ выделяют пространство для временных файлов в стеке вызывающего. У них могут быть разные стратегии в отношении того, сколько места выделяется, как повторно использовать это пространство (это также может зависеть от оптимизации). Они оба могут повторно использовать это пространство по своему усмотрению и, следовательно, небезопасно обращаться к временному адресу, так как мы можем попытаться получить через этот адрес значение, которое, как мы предполагаем, оно все еще имеет (скажем, что-то прямо там написать, а затем попытаться для его получения), в то время как компилятор мог бы повторно использовать его и перезаписать наше значение.