N7>static MType *f(MType *val, int size){
N7> MType tmp;
N7> if (val)
N7> tmp = *val;
N7> else
N7> // do smth to create new
N7> ;
N7> // .....
N7> return &tmp;
N7>}
N7>
N7>т.е. вроде как несмотря на то, что формально tmp и её адреса после выхода из f не существует, реально стек ещё никто не успел (?) испортить и содержимое верное.
Адрес то существует.
Я не знаю как сегодня но в 1998 это был отстой. Было понятие signal. Между выходом из функции и попыткой использовать содержимое по этому адресу может произойти туча всего и этот стек будет перезаписан 10 раз.
Здравствуйте, Nick-77, Вы писали:
N7>есть какой-то структурный тип MType
N7>т.е. вроде как несмотря на то, что формально tmp и её адреса после выхода из f не существует, реально стек ещё никто не успел (?) испортить и содержимое верное.
N7>Хотелось бы узнать у господ знатоков, насколько корректно подобное допущение.
Зачем так делать? Asking for trouble?
Re[3]: Насколько корректно использовать адрес переменной в стеке
Здравствуйте, kov_serg, Вы писали:
_>Это только не в защищённых режимах такая фигня. Бывает
Что мешает прерыванию случиться при работе программы в защищённом режиме? Вот если (асинхронные) прерывания запретили, то, наверное, шансов меньше. Но, во-первых, не уверен насчёт NMI, во-вторых, гипервизор вроде бы может прерывать даже выполнение в нулевом кольце, скажем, по сигналу с карты управления (отдельный Ethernet-интерфейс), и не факт, что он стек не затрёт. Ну и плюс крайне редкий вариант, что перед возвратом вдруг почему-то переполнилось TLB, соответственно, после возврата может произойти (синхронное) прерывание по неотмапленной странице (кода, или данных).
Так что такие возвращения — однозначный поиск неприятностей, для себя или для будущей поддержки. Если не платят зарплату — самое то
Re[3]: Насколько корректно использовать адрес переменной в стеке
Здравствуйте, Nick-77, Вы писали:
N7>Спасибо, но как обойти-то понятно, обидно просто, что нет механизма, позволяющего "немного" (!) подержать стек после возврата из функции
Возврат значения — это и есть немного подержать стек
Альтернативное решение — передать буфер в функцию, как параметр. В том числе, как дефолтный параметр, но там придётся повыкручиваться. http://ideone.com/MTulkq
Я не сторонник такого трюкачества, но вдруг пригодится.
Почему нельзя подержать стек целиком (ну или как-то отдать это на откуп вызываемой функции — сколько там стека придержать)?
Потому что это изрядно меняет протокол работы со стеком. И ради одной, к тому же, очень опасной и ошибкоопасной, фичи, переделывать как бинарную совместимость, так и корректировать стандарт про время жизни объектов, мало кто захочет.
Вот смотри.
Стек состоит из кадров и управляется парой указателей: на начало кадра (для x86 это EBP) и на вершину стека (это ESP).
Функция, создающая свой кадр, делает следующее:
— сохраняет старый указатель кадра на вершине стека, — push ebp
— объявляет вершину стека началом своего кадра, — mov ebp, esp
— заодно, резервирует сколько-то места под локальные переменные — sub esp, N
Или, одной инструкцией, enter N. На других архитектурах и на множественных стеках (например, если стек плавающей арифметики отдельный) делается нечто подобное.
(Перед выходом, достаточно будет прыгнуть на начало кадра — mov esp, ebp — и сделать pop ebp; сокращённо, инструкция leave).
После чего функция обращается к локальным и временным объектам по смещению от ebp. (Отрицательное смещение — локальные переменные, положительное — аргументы).
Однако, если функция не создаёт свой кадр (это один из приёмов оптимизации), либо если она внутри резервирует переменное место на стеке (alloca или сишные переменные массивы), то ей приходится обращаться к некоторым или всем локальным объектам по смещению от esp.
В случае, если все вызываемые из неё функции придерживаются инварианта "esp сразу перед вызовом равен esp сразу после вызова", то компилятору не составит труда следить — насколько esp убежал от точки входа в нашу функцию, и какое смещение нужно подставлять.
Если в языке есть атрибут "вот эта функция шалит и нарушает инвариант", тогда компилятор перед вызовами её будет принудительно создавать кадр на вызывающей стороне, а как работать с кадрами и даже с цепочками кадров, — очевидно. Бегать по ebp, [ebp+1], [ebp+1]+1].
Если же такого атрибута нет, то нарушение инварианта будет огромным сюрпризом.
Опять же, бывают архитектуры, где нет кадров стека, либо где они сделаны очень жёстко. Банки регистров, стеки сопроцессоров. Там инвариант "стек перед вызовом равен стеку до вызова плюс-минус результат" соблюдается неукоснительно.
Конечно, было бы круто, если бы прямо из коробки и задёшево можно было обеспечивать инкапсуляцию и полиморфизм
Base f() {
if (rand()) return Derived1(); // сейчас мы отхватим срезку до Base, а хотелось быelse return Derived2(); // вернуть разные типы, да ещё и разных размеров
}
int(g())[] {
int t[rand()]; // создали и заполнили массив произвольной (в рамках разумного) длиныreturn t; // сейчас мы отхватим деградацию до указателя и убъём локальную переменную
}
Но обычно такие вещи делаются через кучу. Даже в языках, где работа с кучей неявная, — например, в функциональных.
Перекуём баги на фичи!
Re[7]: Насколько корректно использовать адрес переменной в с
Здравствуйте, watchmaker, Вы писали:
W>То есть там просто вместо готового объекта передаётся указатель на alignas(T) char buffer[sizeof(T)], в который потом можно сделать placement_new и вернуть указатель на новый объект.
Кстати про aligned storage. (И, в частности, std::aligned_storage, чтобы не переизобретать).
Деструктор-то ведь тоже придётся вызывать вручную.
Более правильный подход — использовать в качестве буфера std::optional или его аналоги.
Перекуём баги на фичи!
Re[8]: Насколько корректно использовать адрес переменной в с
N7>тык у меня ж речь о голом С, а он разве подправляет заголовки функций, ведь получается, что допустим struct Mtype f(int a); должна быть преобразована НЕЯВНО в struct * Mtype f(Struct *val, int a);
N7>что, мягко говоря, вызывает вопросы как это чудо будет линоваться, особенно с не-С кодом, с каким-нить пайтоном...
N7>?
Это правильный вопрос. Конкретный ответ на него даёт ABI. И обычно ответ: все должны делать эту замену (неявно, при совпадении некоторых условий).
И такая замена действительно происходит и при сборке C++, и при сборке голого C, и даже при сборке бинарных модулей Python.
Ведь, конечно, верно, что если бы это не происходило во всех языках одновременно и синхронно, то они бы не смогли корректно линковаться между собой.
Просто что касается того же Python, то внутри у него (у CPython) возврат структур сделан через собственную виртуальную машину и там вопрос про стек немного не имеет смысла. Но если нужно вызвать из Python какой-нибудь бинарный модуль, написанный на C, который при этом возвращает структуру, то такая замена произойдёт.
Но, конечно, в самом интерпретаторе Python об таких деталях не особо упоминают просто из-за того, что это немного offtopic. А Python делегирует эту низкоуровневую работу либо соответствующему компилятору (если собираем модуль на C, например), либо libffi (если подключаем модуль в run-time).
То ли в 17м, то ли в 20 стандарте NRVO обязали делать. Теперь можно возвращать moveonly типы.
Здравствуйте, Кодт, Вы писали:
К>Если цель не в том, чтобы (контролируемо) выстрелить себе в ногу, а в том, чтобы вернуть значение по указателю, то К>1) понадеяться на оптимизации NRVO
Re[2]: Насколько корректно использовать адрес переменной в стеке
т.е. вроде как несмотря на то, что формально tmp и её адреса после выхода из f не существует, реально стек ещё никто не успел (?) испортить и содержимое верное.
Хотелось бы узнать у господ знатоков, насколько корректно подобное допущение.
Re: Насколько корректно использовать адрес переменной в стеке
Здравствуйте, watchmaker, Вы писали:
К>>Возврат значения — это и есть немного подержать стек К>>Альтернативное решение — передать буфер в функцию, как параметр. В том числе, как дефолтный параметр, но там придётся повыкручиваться. К>>Я не сторонник такого трюкачества, но вдруг пригодится.
W>Честно говоря, если прочитать например SystemV ABI, то можно увидеть, что это автоматически происходит для всех структур (размер которых больше некоторого порогового значения). W>То есть всё это извращение с передачей дефолтного параметра не нужно.
Именно это я и имел в виду, — что возврат значения и есть придерживание стека. Вызывающая сторона сама размещает буфер и отдаёт его как неявный параметр.
W>Поэтому такой трюк с дефолтным параметром сугубо вредный: он явно выражает то, что и так записано в ABI как необходимое действие.
Насчёт сугубо-вредности — тут можно пообсуждать.
Во-первых, мы не надеемся на NRVO, а вручную гарантируем это. И заодно, снимаем требование о копируемости-перемещаемости с типа. (Правда, добавляя некоторую дефолтную инициализацию и вторую фазу).
Во-вторых, с этим же трюком мы можем отдавать буфер произвольного вида. Ну самое простое,
Здравствуйте, watchmaker, Вы писали:
К>>Во-первых, мы не надеемся на NRVO W>Тут как бы дело в том, что такого поведения требует ABI, а не стандарт C++. То есть NRVO тут вообще не при чём. Так делать компилятор обязан всегда, ибо программа должна удовлетворять требованиям из обоих этих документов.
ABI требует, чтобы вызывающая сторона передала буфер в функцию и получила туда ответ.
А будет ли это сделано задёшево или задорого, об этом ничего не сказано. И более того, — уже стандарт требует, чтобы компилятору разрешили делать это задорого (тип должен быть Copyable/Moveable), одновременно сам же разрешая компилятору делать это задёшево (NRVO).
К>> И заодно, снимаем требование о копируемости-перемещаемости с типа. (Правда, добавляя некоторую дефолтную инициализацию и вторую фазу). W>Ага. Правда компилятор без этого трюка ещё и сам автоматически разрешит ситуацию, когда, например, у класса нет дефолтного инициализатора.
Ну уж тут "Направо пойдёшь — коня потеряешь, налево пойдёшь — сама тебя прибью. Твоя Василиса"
Конечно, рукосипедить нужно только тогда, когда исчерпаны обычные решения. То есть, если у нас жёсткие ограничения на свойства типа, или если профайлер нам здесь показал, что NRVO не выполняется, и что нам от этого плохо.
Перекуём баги на фичи!
Re: Насколько корректно использовать адрес переменной в стеке
...
N7>т.е. вроде как несмотря на то, что формально tmp и её адреса после выхода из f не существует, реально стек ещё никто не успел (?) испортить и содержимое верное.
N7>Хотелось бы узнать у господ знатоков, насколько корректно подобное допущение.
Если после выхода из функции произойдёт прерывание , то его обработчик всё затрёт .
Re[2]: Насколько корректно использовать адрес переменной в стеке
Здравствуйте, icWasya, Вы писали: W>Здравствуйте, Nick-77, Вы писали: W>... N7>>т.е. вроде как несмотря на то, что формально tmp и её адреса после выхода из f не существует, реально стек ещё никто не успел (?) испортить и содержимое верное.
N7>>Хотелось бы узнать у господ знатоков, насколько корректно подобное допущение.
Еще бывает что стек растёт в другую сторону на некоторых архитектурах.
Скрытый текст
// ptsk.h : popup multitasking#ifndef __PTSK_H__
#define __PTSK_H__
#include <setjmp.h>
#include"ticks.h"// any real time clock#ifdef __cplusplus
extern"C" {
#endif
typedef struct ptsk_tcb_tag {
void (*task)(void*);
void *args;
int stack_size;
jmp_buf ctx;
struct ptsk_tcb_tag* next;
struct ptsk_tcb_tag* prev;
} ptsk_tcb_t;
void ptsk_init(void);
void ptsk_done(void);
void ptsk_idle(void); // give control to next taskvoid ptsk_addtask(ptsk_tcb_t *task); // create new taskvoid ptsk_deltask(ptsk_tcb_t *task); // if current it'll dievoid ptsk_die(void); // kill current task
ptsk_tcb_t* ptsk_getcurrent(); // get current task descriptorextern int ptsk_active;
void ptsk_sleep(ticks_t ticks);
void ptsk_sleep_till(ticks_t time);
/*
usage:
#include <stdio.h>
#include "ptsk.h"
void task1(void* arg) {
for(int i=0;i<=9;++i) {
printf("task1 \ti=%d\n",i);
ptsk_idle();
}
}
void task2(void* arg) {
for(int i=0;i<=7;++i) {
printf("task2 \t\ti=%d\n",i);
ptsk_sleep(500);
}
}
void task3(void* arg) {
for(int i=0;i<=5;++i) {
printf("task3 \t\t\ti=%d\n",i);
ptsk_sleep(1000);
}
}
ptsk_tcb_t t1={task1,(void*)1,1024};
ptsk_tcb_t t2={task2,(void*)2,1024};
ptsk_tcb_t t3={task3,(void*)3,1024};
void main() {
ptsk_init();
ptsk_addtask(&t1);
ptsk_addtask(&t2);
ptsk_addtask(&t3);
ptsk_idle();
ptsk_done();
}
*/#ifdef __cplusplus
}
#endif
#endif// __PTSK_H__
// ptsk.c#include"ptsk.h"#ifdef __cplusplus
extern"C" {
#endif
enum {
PTSK_1ST =0,
PTSK_WAKE=1,
PTSK_NEW =2,
PTSK_RET =3
};
int ptsk_active=0;
ptsk_tcb_t* ptsk_head;
ptsk_tcb_t* ptsk_tail;
ptsk_tcb_t* ptsk_curr;
jmp_buf ptsk_last;
jmp_buf ptsk_main;
void ptsk_init(void) {
ptsk_active=0;
ptsk_head=ptsk_tail=ptsk_curr=0;
}
void ptsk_done(void) {
if (ptsk_active) longjmp(ptsk_main,PTSK_RET);
}
void ptsk_panic(void) {
// exit(0);for(;;) {}
}
void ptsk_switch(void) {
ptsk_curr=ptsk_curr->next;
if (!ptsk_curr) {
ptsk_curr=ptsk_head;
if (!ptsk_curr) ptsk_done();
}
longjmp(ptsk_curr->ctx,PTSK_WAKE);
}
void ptsk_die(void) {
if (!ptsk_curr) { ptsk_done(); ptsk_panic(); return; } // panicif (ptsk_curr->prev) ptsk_curr->prev->next=ptsk_curr->next; else ptsk_head=ptsk_curr->next;
if (ptsk_curr->next) ptsk_curr->next->prev=ptsk_curr->prev; else ptsk_tail=ptsk_curr->prev;
ptsk_switch();
}
void ptsk_deltask(ptsk_tcb_t *task) {
if (ptsk_curr==task) ptsk_die();
if (task->prev) task->prev->next=task->next; else ptsk_head=task->next;
if (task->next) task->next->prev=task->prev; else ptsk_tail=task->prev;
}
int ptsk_stackalloc(int arg) {
volatile int dummy; ptsk_tcb_t* tsk; int sz;
static int* stk;
if (arg==1) stk=(int*)&dummy;
sz=(int*)&dummy-stk; if (sz<0) sz=-sz;
if (sz<ptsk_tail->stack_size) ptsk_stackalloc(0);
tsk=ptsk_tail;
if (setjmp(ptsk_last)==PTSK_NEW) ptsk_stackalloc(1);
switch(setjmp(tsk->ctx)) {
case PTSK_1ST: longjmp(ptsk_main,PTSK_RET);
case PTSK_WAKE: tsk->task(tsk->args);
}
ptsk_die(); return dummy;
}
void ptsk_addtask(ptsk_tcb_t *task) {
ptsk_tcb_t* last;
last=ptsk_tail;
task->prev=ptsk_tail;
task->next=0;
if (ptsk_tail) ptsk_tail->next=task; else ptsk_head=task;
ptsk_tail=task;
switch(setjmp(ptsk_main)) {
case PTSK_1ST: if (last) longjmp(ptsk_last,PTSK_NEW); else ptsk_stackalloc(1);
//case PTSK_RET: break;
}
}
void ptsk_idle(void) {
if (!ptsk_curr) {
ptsk_curr=ptsk_head; if (!ptsk_curr) return;
ptsk_active=1;
switch(setjmp(ptsk_main)) {
case PTSK_1ST: ptsk_curr->task(ptsk_curr->args); ptsk_die();
//case PTSK_RET: break;
}
ptsk_active=0;
return;
}
switch(setjmp(ptsk_curr->ctx)) {
case PTSK_1ST: ptsk_switch();
//case PTSK_WAKE: break;
}
}
ptsk_tcb_t* ptsk_getcurrent() {
return ptsk_curr;
}
void ptsk_sleep_till(ticks_t time) {
do {
ptsk_idle();
} while(time-getticks()>0);
}
void ptsk_sleep(ticks_t ticks) {
ptsk_sleep_till(getticks()+ticks);
}
#ifdef __cplusplus
}
#endif
W>Если после выхода из функции произойдёт прерывание , то его обработчик всё затрёт .
Это только не в защищённых режимах такая фигня. Бывает
Re: Насколько корректно использовать адрес переменной в стеке
N7>static MType *f(MType *val, int size){
N7> MType tmp;
N7> if (val)
N7> tmp = *val;
N7> else
N7> // do smth to create new
N7> ;
N7> // .....
N7> return &tmp;
N7>}
N7>
N7>т.е. вроде как несмотря на то, что формально tmp и её адреса после выхода из f не существует, реально стек ещё никто не успел (?) испортить и содержимое верное.
Теоретически реализации никто не мешает вернуть из этой функции нулевой указатель вместо адреса умершего объекта, задокументировав, что invalid pointer value иногда может вести себя, как null pointer value. И, кстати, это был бы вполне резонный подход, дабы сразу бить по рукам тех, кто попытается что-либо прочитать или записать по "неправильному" адресу.
Re[4]: Насколько корректно использовать адрес переменной в стеке
Здравствуйте, cures, Вы писали:
_>>Это только не в защищённых режимах такая фигня. Бывает
C>Что мешает прерыванию случиться при работе программы в защищённом режиме?
Мне кажется товарищ имеет ввиду что в защищенных режимах переключается стек. Но насколько помню я (а изучал я I32 в 1993 году) возможны прерывания и с переключением стека и без оного.
Re: Насколько корректно использовать адрес переменной в стеке
Здравствуйте, Mr.Delphist, Вы писали:
MD>Здравствуйте, Nick-77, Вы писали:
N7>>есть какой-то структурный тип MType
N7>>т.е. вроде как несмотря на то, что формально tmp и её адреса после выхода из f не существует, реально стек ещё никто не успел (?) испортить и содержимое верное.
N7>>Хотелось бы узнать у господ знатоков, насколько корректно подобное допущение.
MD>Зачем так делать? Asking for trouble?
Что-то типа проверки границ допустимого
Re[2]: Насколько корректно использовать адрес переменной в стеке
NI>Теоретически реализации никто не мешает вернуть из этой функции нулевой указатель вместо адреса умершего объекта, задокументировав, что invalid pointer value иногда может вести себя, как null pointer value. И, кстати, это был бы вполне резонный подход, дабы сразу бить по рукам тех, кто попытается что-либо прочитать или записать по "неправильному" адресу.
Никто, кроме стандарта, вроде.
Re[2]: Насколько корректно использовать адрес переменной в стеке
Здравствуйте, icWasya, Вы писали:
N7>>т.е. вроде как несмотря на то, что формально tmp и её адреса после выхода из f не существует, реально стек ещё никто не успел (?) испортить и содержимое верное.
W>Если после выхода из функции произойдёт прерывание , то его обработчик всё затрёт .
Исчерпывающе, спасибо!
Re[3]: Насколько корректно использовать адрес переменной в стеке
Nick-77:
NI>>Теоретически реализации никто не мешает вернуть из этой функции нулевой указатель вместо адреса умершего объекта, задокументировав, что invalid pointer value иногда может вести себя, как null pointer value. И, кстати, это был бы вполне резонный подход, дабы сразу бить по рукам тех, кто попытается что-либо прочитать или записать по "неправильному" адресу.
N7>Никто, кроме стандарта, вроде.
C11 — 6.2.4/2:
If an object is referred to outside of its lifetime, the behavior is undefined. The value of a pointer becomes indeterminate when the object it points to (or just past) reaches the end of its lifetime.
C11 — 3.19.2:
indeterminate value
either an unspecified value or a trap representation
C11 — 3.19.3:
unspecified value
valid value of the relevant type where this International Standard imposes no requirements on which value is chosen in any instance
Т.е. в C указатель на умерший объект вполне может стать нулевым.
C++14 плохо описывает способы инвалидации указателей, но при подготовке C++17 над этой проблемой уже успели поработать.
C++ N4660 [basic.stc]/4:
When the end of the duration of a region of storage is reached, the values of all pointers representing the address of any part of that region of storage become invalid pointer values (6.9.2). Indirection through an invalid pointer value and passing an invalid pointer value to a deallocation function have undefined behavior. Any other use of an invalid pointer value has implementation-defined behavior. [Footnote: Some implementations might define that copying an invalid pointer value causes a system-generated runtime fault.]
C++ N4660 [defns.impl.defined]
implementation-defined behavior
behavior, for a well-formed program construct and correct data, that depends on the implementation and that each implementation documents
Результат любой операции над невалидным указателем (даже простого копирования такого указателя) определяется реализацией — какой результат она захочет сделать, такой и будет. В частности, поведение невалидного указателя может быть идентично поведению нулевого указателя.
Но это только одна из потенциально возможных причин, по которым данный хак может оказаться нерабочим. Когда компилятор захочет соптимизировать такой код, там может получиться что угодно. И если с текущей версией компилятора видимых проблем не наблюдается, то при обновлении на новую версию можно получить сюрприз.
Re[4]: Насколько корректно использовать адрес переменной в стеке
Здравствуйте, Nick-77, Вы писали:
N7>Вот в таком случае:
<> N7>Хотелось бы узнать у господ знатоков, насколько корректно подобное допущение.
Если цель не в том, чтобы (контролируемо) выстрелить себе в ногу, а в том, чтобы вернуть значение по указателю, то
1) понадеяться на оптимизации NRVO
2) поклясться на времени жизни
3) сделать вот так
MType f_construct(MType* val, int size) { // может быть и не инлайновым
MType tmp;
// пост-инициализация переменной tmp
.....
// NRVO, прииди!return tmp;
}
#define f_by_pointer(val, size) (&(f_construct((val), (size))))
// ну и пример использования
MType v = f_construct(f_by_pointer(nullptr, 123), 456);
// хотя можно и без макроса обойтись
MType w = f_construct(&f_construct(nullptr, 123), 456);
Обрати внимание, что f_construct с самого начала (ещё будучи макросом) возвращал значение. Так пускай же именно это делает функция, без лишних танцев с бубном.
Перекуём баги на фичи!
Re[2]: Насколько корректно использовать адрес переменной в стеке
Здравствуйте, Кодт, Вы писали:
К>Здравствуйте, Nick-77, Вы писали:
N7>>Вот в таком случае: К><> N7>>Хотелось бы узнать у господ знатоков, насколько корректно подобное допущение.
К>Если цель не в том, чтобы (контролируемо) выстрелить себе в ногу, а в том, чтобы вернуть значение по указателю, то К>1) понадеяться на оптимизации NRVO К>2) поклясться на времени жизни К>3) сделать вот так К>
К>MType f_construct(MType* val, int size) { // может быть и не инлайновым
К> MType tmp;
К> // пост-инициализация переменной tmp
К> .....
К> // NRVO, прииди!
К> return tmp;
К>}
К>#define f_by_pointer(val, size) (&(f_construct((val), (size))))
К>// ну и пример использования
К>MType v = f_construct(f_by_pointer(nullptr, 123), 456);
К>// хотя можно и без макроса обойтись
К>MType w = f_construct(&f_construct(nullptr, 123), 456);
К>
К>Обрати внимание, что f_construct с самого начала (ещё будучи макросом) возвращал значение. Так пускай же именно это делает функция, без лишних танцев с бубном.
Спасибо, но как обойти-то понятно, обидно просто, что нет механизма, позволяющего "немного" (!) подержать стек после возврата из функции
Re[3]: Насколько корректно использовать адрес переменной в стеке
Здравствуйте, Nick-77, Вы писали:
N7> обидно просто, что нет механизма, позволяющего "немного" (!) подержать стек после возврата из функции
Конечно же такой механизм существует: например на архитектуре x86-64 он называется RedZone. И популярные компиляторы clang и gcc вполне успешно им пользуются.
Только философия тут другая: программисту не нужно задумываться об особенностях реализации RedZone или аналогов на конкретной архитектуре, вместо этого от него требуется лишь написать корректный код на C++.
А уже потом компилятор (точно зная всякие неочевидные детали о том как работает стек на конкретной платформе: как он взаимодействует с прерываниями, когда инвалидируется и.т.п.) выкинет ненужные действия из машинного кода.
Re[4]: Насколько корректно использовать адрес переменной в стеке
Здравствуйте, Кодт, Вы писали:
К>Возврат значения — это и есть немного подержать стек :)
К>Альтернативное решение — передать буфер в функцию, как параметр. В том числе, как дефолтный параметр, но там придётся повыкручиваться. К>http://ideone.com/MTulkq К>Я не сторонник такого трюкачества, но вдруг пригодится.
Честно говоря, если прочитать например SystemV ABI, то можно увидеть, что это автоматически происходит для всех структур (размер которых больше некоторого порогового значения).
То есть всё это извращение с передачей дефолтного параметра не нужно. Программисту достаточно написать просто
BigStruct foo() {
BigStruct r;
...
return r;
}
И компилятор сам сделает замену: зарезервирует память в вызывающей функции и возвратит из функции указатель на r.
И это требование! То есть так поступать компилятор обязан даже при полностью выключенной оптимизации. И, конечное же, все компиляторы так и поступают: Пруф.
Поэтому такой трюк с дефолтным параметром сугубо вредный: он явно выражает то, что и так записано в ABI как необходимое действие.
Единственный нюанс тут в том, что есть несколько разных ABI — формально нужно смотреть на версию своей для платформы. Но, к счастью, в вопросе об возврате структур по значению они практически все единогласны.
Re[6]: Насколько корректно использовать адрес переменной в с
W>>Поэтому такой трюк с дефолтным параметром сугубо вредный: он явно выражает то, что и так записано в ABI как необходимое действие.
К>Насчёт сугубо-вредности — тут можно пообсуждать.
К>Во-первых, мы не надеемся на NRVO
Тут как бы дело в том, что такого поведения требует ABI, а не стандарт C++. Так делать компилятор обязан всегда, ибо программа должна удовлетворять требованиям из обоих этих документов.
То есть NRVO тут важно только из-за того, что ускоряет внутренности функции и избавляет от временных переменных, но на время жизни памяти оно не влияет.
То есть если рассматривать ситуацию с точки зрения только C++, то про конкретное устройство стека (и его взаимодействие с сигналами, прерываниями и прочем) практически ничего нельзя сказать, и поэтому нельзя утверждать, что такой код даст какой-то выигрыш.
Если же подключить знания об устройстве стека (которое описано в ABI), то выясняется, что такой код заведомо не нужен.
Вот этот последний ненужный код я и назвал вредным за то, что он просто сложнее при одновременном отсутствии какой-либо выгоды.
К> И заодно, снимаем требование о копируемости-перемещаемости с типа. (Правда, добавляя некоторую дефолтную инициализацию и вторую фазу).
Ага. Правда компилятор без этого трюка ещё и сам автоматически разрешит ситуацию, когда, например, у класса нет дефолтного инициализатора.
То есть там просто вместо готового объекта передаётся указатель на alignas(T) char buffer[sizeof(T)], в который потом можно сделать placement_new и вернуть указатель на новый объект.
Здравствуйте, watchmaker, Вы писали:
W>Здравствуйте, Кодт, Вы писали:
К>>Возврат значения — это и есть немного подержать стек
К>>Альтернативное решение — передать буфер в функцию, как параметр. В том числе, как дефолтный параметр, но там придётся повыкручиваться. К>>http://ideone.com/MTulkq К>>Я не сторонник такого трюкачества, но вдруг пригодится.
W>Честно говоря, если прочитать например SystemV ABI, то можно увидеть, что это автоматически происходит для всех структур (размер которых больше некоторого порогового значения). W>То есть всё это извращение с передачей дефолтного параметра не нужно. Программисту достаточно написать просто
W>И компилятор сам сделает замену: зарезервирует память в вызывающей функции и возвратит из функции указатель на r. W>И это требование! То есть так поступать компилятор обязан даже при полностью выключенной оптимизации. И, конечное же, все компиляторы так и поступают: Пруф. W>Поэтому такой трюк с дефолтным параметром сугубо вредный: он явно выражает то, что и так записано в ABI как необходимое действие.
вот тут я перестал понимать, ведь это самое r и будет в стеке функции, который помрёт, т.е. от возврата указателя толку мало, или там будет какая-то неявная обёртка в memcpy?
Re[6]: Насколько корректно использовать адрес переменной в стеке
Здравствуйте, Nick-77, Вы писали:
N7>вот тут я перестал понимать, ведь это самое r и будет в стеке функции, который помрёт, т.е. от возврата указателя толку мало, или там будет какая-то неявная обёртка в memcpy?
Память под BigStruct будет выделена в стеке вызывающей функции. Поэтому эта память будет жить и после завершения вызываемой функции. И поэтому функция может вернуть указатель на эту память, так как этот указатель останется валидным даже после завершения foo.
И по этому указателю компилятор будет хранить r (на момент выхода из функции).
Уточнение в скобках важно.
То есть у функции foo есть участок в стеке, который заведомо переживёт время жизни самой foo, и в которой должна размещаться переменная r.
При этом, если переменная r размещается в этом месте всё время своей жизни, то это называется NRVO. Иначе же допускается, что переменная может быть сконструирована на собственном стеке foo, а потом перед выходом скопирована в указанную память, в том числе и через memcpy (эта ситуация называется "NRVO не сработал").
В общем-то Кодт очень хорошо написал выше вручную код, который компилятор сам делает. Можно сверится с ним.
Re[4]: Насколько корректно использовать адрес переменной в стеке
Здравствуйте, cures, Вы писали:
C>Что мешает прерыванию случиться при работе программы в защищённом режиме? Вот если (асинхронные) прерывания запретили, то, наверное, шансов меньше. Но, во-первых, не уверен насчёт NMI, во-вторых, гипервизор вроде бы может прерывать даже выполнение в нулевом кольце, скажем, по сигналу с карты управления (отдельный Ethernet-интерфейс), и не факт, что он стек не затрёт. Ну и плюс крайне редкий вариант, что перед возвратом вдруг почему-то переполнилось TLB, соответственно, после возврата может произойти (синхронное) прерывание по неотмапленной странице (кода, или данных).
В защищенных режимах, когда происходит прерывание, то обычно сначала аппаратура переключает стек на стек ядра, а потом уже в него гадит. Что довольно логично, было бы обидно, если прерывание произошло, а юзер испортил свой указатель стека так, что туда ничего не запишешь.
C>Так что такие возвращения — однозначный поиск неприятностей, для себя или для будущей поддержки. Если не платят зарплату — самое то
Это да.
Re: Насколько корректно использовать адрес переменной в стеке
Здравствуйте, watchmaker, Вы писали:
W>Здравствуйте, Nick-77, Вы писали:
N7>>вот тут я перестал понимать, ведь это самое r и будет в стеке функции, который помрёт, т.е. от возврата указателя толку мало, или там будет какая-то неявная обёртка в memcpy?
W>Память под BigStruct будет выделена в стеке вызывающей функции. Поэтому эта память будет жить и после завершения вызываемой функции. И поэтому функция может вернуть указатель на эту память, так как этот указатель останется валидным даже после завершения foo. W>И по этому указателю компилятор будет хранить r (на момент выхода из функции). W>Уточнение в скобках важно.
W>То есть у функции foo есть участок в стеке, который заведомо переживёт время жизни самой foo, и в которой должна размещаться переменная r.
W>При этом, если переменная r размещается в этом месте всё время своей жизни, то это называется NRVO. Иначе же допускается, что переменная может быть сконструирована на собственном стеке foo, а потом перед выходом скопирована в указанную память, в том числе и через memcpy (эта ситуация называется "NRVO не сработал").
W>В общем-то Кодт очень хорошо написал выше вручную код, который компилятор сам делает. Можно сверится с ним.
тык у меня ж речь о голом С, а он разве подправляет заголовки функций, ведь получается, что допустим struct Mtype f(int a); должна быть преобразована НЕЯВНО в struct * Mtype f(Struct *val, int a);
что, мягко говоря, вызывает вопросы как это чудо будет линоваться, особенно с не-С кодом, с каким-нить пайтоном...
?
Re[5]: Насколько корректно использовать адрес переменной в с
Здравствуйте, Pzz, Вы писали:
Pzz>В защищенных режимах, когда происходит прерывание, то обычно сначала аппаратура переключает стек на стек ядра, а потом уже в него гадит.
В оригинальном сообщении нигде не сказано что речь идет о пользовательской программе. Как известно операционные системы тоже пишутся на C. И если мы уже в ядре то фраза "переключает стек на стек ядра" становится непонятной.
А при работе в режиме ядра возможны прерывания как с переключением стека так и без него о чем я уже писал в сообщении выше.
• If the handler procedure is going to be executed at a numerically lower privilege level, a stack switch occurs.
When the stack switch occurs:
a. The segment selector and stack pointer for the stack to be used by the handler are obtained from the TSS for the currently executing task. On this new stack, the processor pushes the stack segment selector and stack pointer of the interrupted procedure.
b. The processor then saves the current state of the EFLAGS, CS, and EIP registers on the new stack (see Figures 6-4).
c. If an exception causes an error code to be saved, it is pushed on the new stack after the EIP value.
• If the handler procedure is going to be executed at the same privilege level as the interrupted procedure:
a. The processor saves the current state of the EFLAGS, CS, and EIP registers on the current stack (see Figures 6-4).
b. If an exception causes an error code to be saved, it is pushed on the current stack after the EIP value.
...
To return from an exception- or interrupt-handler procedure, the handler must use the IRET (or IRETD) instruction.
The IRET instruction is similar to the RET instruction except that it restores the saved flags into the EFLAGS register. The IOPL field of the EFLAGS register is restored only if the CPL is 0. The IF flag is changed only if the CPL is less than or equal to the IOPL. See Chapter 3, "Instruction Set Reference, A-L," of the Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 2A, for a description of the complete operation performed by the IRET instruction.
If a stack switch occurred when calling the handler procedure, the IRET instruction switches back to the interrupted procedure’s stack on the return.
Здравствуйте, Лазар Бешкенадзе, Вы писали:
ЛБ>Здравствуйте, Nick-77, Вы писали:
N7>>
N7>>static MType *f(MType *val, int size){
N7>> MType tmp;
N7>> if (val)
N7>> tmp = *val;
N7>> else
N7>> // do smth to create new
N7>> ;
N7>> // .....
N7>> return &tmp;
N7>>}
N7>>
N7>>т.е. вроде как несмотря на то, что формально tmp и её адреса после выхода из f не существует, реально стек ещё никто не успел (?) испортить и содержимое верное.
ЛБ>Адрес то существует.
ЛБ>Я не знаю как сегодня но в 1998 это был отстой. Было понятие signal. Между выходом из функции и попыткой использовать содержимое по этому адресу может произойти туча всего и этот стек будет перезаписан 10 раз.
Здравствуйте, Nick-77, Вы писали:
N7>Вот в таком случае:
N7>т.е. вроде как несмотря на то, что формально tmp и её адреса после выхода из f не существует, реально стек ещё никто не успел (?) испортить и содержимое верное.
N7>Хотелось бы узнать у господ знатоков, насколько корректно подобное допущение.
с вероятностью (90+x)% всё будет впорядке. Но с вероятностью (10 — x) % произойдут невероятные баги, которые невозможно будет отследить. И возникнут они не сразу, а через год существования проекта. И получится такой хтонический звиздец, что выть будет вся команда, сроки будут провалены, фирма получит убыток, а виновный кодер... В лучшем случае получит во все дырки, и ещё дополнительную просверлят в боку. А в худшем пойдёт доучивать материал для собеседований. А ещё компилятор может соптимизировать эту конструкцию неожиданным образом. Например, кланговский компилятор не стесняется даже волэйтил игнорировать в некоторых случаях.
Поэтому так никто не делает.
Re[3]: Насколько корректно использовать адрес переменной в стеке
swingus:
S>То ли в 17м, то ли в 20 стандарте NRVO обязали делать.
Откуда инфа? В C++17 такого точно нет.
S>Теперь можно возвращать moveonly типы.
Уже в C++11 можно возвращать объекты классового типа, где нет вообще ни одного non-deleted конструктора; с введением в C++17 новых правил для prvalues появилось больше возможных вариантов сформировать валидное return statement. Но NRVO работает с lvalues, и к ней такие новшества никакого отношения не имеют.
Re[4]: Насколько корректно использовать адрес переменной в стеке
Здесь я вижу лёгкое безумие с вашей стороны. Если нет ни одного non-deleted конструктора (и оператора =), то это вероятно moveable type, а скорее всего copyable, всё зависит от членов класса. А взял я отсюда, например (на самом деле чуть ли не первая ссылка в выдаче):
Здравствуйте, N. I., Вы писали:
NI>swingus:
S>>То ли в 17м, то ли в 20 стандарте NRVO обязали делать.
NI>Откуда инфа? В C++17 такого точно нет.
S>>Теперь можно возвращать moveonly типы.
NI>Уже в C++11 можно возвращать объекты классового типа, где нет вообще ни одного non-deleted конструктора; с введением в C++17 новых правил для prvalues появилось больше возможных вариантов сформировать валидное return statement. Но NRVO работает с lvalues, и к ней такие новшества никакого отношения не имеют.
Re[5]: Насколько корректно использовать адрес переменной в стеке
swingus:
S>Здесь я вижу лёгкое безумие с вашей стороны. Если нет ни одного non-deleted конструктора (и оператора =), то это вероятно moveable type, а скорее всего copyable, всё зависит от членов класса.
A second consequence is that nothing changes for NVRO in C++17 with guaranteed copy elision. This is because, as mentioned before, the change only involves prvalues. With NVRO, the named value is a glvalue. The authors of the paper acknowledge this but chose to leave it out of scope.
While we believe that reliable NRVO is an important feature to allow reasoning about performance, the cases where NRVO is possible are subtle and a simple guarantee is difficult to give.