Комментарии 40
Я не написал
return std::move(person);
и не надо его там писать. Никакого "барьера оптимизации" в случае каких-то бы то ни было линковок не происходит. А вот std::move(person) как раз создаст барьер оптимизации
https://godbolt.org/z/T9jTE965c
Удостоверяйтесь в том, что вы вызываете деструктор и освобождаете память объекта из того же модуля, из которого были захвачена память и вызван конструктор.
Кажется это противоречит совету использовать unique_ptr
P.S. так как это NRVO и в статье есть ещё return, то можно просто на месте создать person для гарантированной оптимизации
P.P.S в статье раз 7 фигурирует std::move на const char*, видимо опечатка
Почему не происходит?
Никакого "барьера оптимизации"
Можете, пожалуйста, пояснить более подробно?
Я исхожу из того, что вызов функции make_person
происходит из другого программного модуля, из-за чего copy elision - невозможно.
Если делать взызов этой функции из того же модуля, то и RVO и NRVO, конечно же, сработает, что верно иллюстрирует ваш пример из godbolt.
Но сама функция осуществляет возврат, это бы значило, что она должна иметь 2 версии - одну на случай когда её зовут из длл, другую на случай если зовут не из длл. Очевидно этого не должно быть, функция одна
Пожалуйста, ознакомьтесь с https://stackoverflow.com/questions/23777863/do-rvo-and-copy-elision-only-work-within-one-compilation-unit-or-not.
RVO возможна только в одной единице трансляции (если брать в расчёт специальные опции линковщика). Поэтому, в общем случае, RVO и NRVO при пересечении бинарной границы между разными модулями - невозможны.
Пожалуйста, ознакомьтесь с https://stackoverflow.com/questions/23777863/do-rvo-and-copy-elision-only-work-within-one-compilation-unit-or-not.
Простите, а когда вы черпаете вот эти свои знания из советских газет наподобие StackOverflow, вы вообще проверяете то, что там пишут, или нет? А то там пишут очень много чуши.
Несложная проверка
gentoo ~/Temp/dlltest $ cat lib.h
#pragma once
struct S
{
S();
S(const S&);
S(S&&);
int i = 0;
};
S foo1();
S foo2();
int bar();
gentoo ~/Temp/dlltest $ cat lib.cpp
#include "lib.h"
#include <iostream>
int bar()
{
S s1 = foo1();
std::cout << "----------" << std::endl;
S s2 = foo2();
return s1.i + s2.i;
}
gentoo ~/Temp/dlltest $ cat main.cpp
#include "lib.h"
#include <iostream>
#include <utility>
S::S()
{
std::cout << "S()" << std::endl;
}
S::S(const S&)
{
std::cout << "S(const S&)" << std::endl;
}
S::S(S&&)
{
std::cout << "S(S&&)" << std::endl;
}
S foo1()
{
std::cout << "foo1()" << std::endl;
S s;
return s;
}
S foo2()
{
std::cout << "foo2()" << std::endl;
S s;
return std::move(s);
}
int main()
{
return bar();
}
# Не применяем -flto, плюс на всякий случай делаем динамическую библиотеку
gentoo ~/Temp/dlltest $ g++ -Wall -fPIC -shared -o lib.so lib.cpp
# После этой строчки никакие дополнительные оптимизации в lib.so не выполняются
# "Код библиотеки фиксирован и полностью собран" (C)
# Не применяем -flto
gentoo ~/Temp/dlltest $ g++ -Wall -c -o main.o main.cpp
main.cpp: In function ‘S foo2()’:
main.cpp:34:21: warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]
34 | return std::move(s);
| ~~~~~~~~~^~~
main.cpp:34:21: note: remove ‘std::move’ call
# Не применяем -flto
gentoo ~/Temp/dlltest $ g++ -o prog main.o lib.so
# Проверяем, что динамическая библиотека реально используется
gentoo ~/Temp/dlltest $ ./prog
./prog: error while loading shared libraries: lib.so: cannot open shared object file: No such file or directory
gentoo ~/Temp/dlltest $ LD_LIBRARY_PATH=. ./prog
foo1()
S() <- RVO вполне себе сработало внутри .so
----------
foo2()
S()
S(S&&) <- RVO не сработало, так как мы явно попросили об этом через std::move()
Прошу, пожалуйста, обратить внимание:
Поэтому, в общем случае, RVO и NRVO при пересечении бинарной границы между разными модулями - невозможны.
Простите, но это всего лишь ваше утверждение, ни на чем не основанное. Ну кроме фантазий со Stack Overflow.
Извините, ваше утверждение основано на эксперименте с одним компилятором в одном конкретном окружении. Если вы претендуете на научный подход, то, вам нужна рандомизированная выборка, или ссылка на стандарт.
Вы всегда можете этим заняться, я буду рад дополнить статью результатами вашего исследования, коллега.
Извините, но ваше утверждение вообще ни на чем не основано. Вам необходимо его чем-то подкрепить, а то иначе это просто слова.
Прошу прощения, я выразился не до конца ясно. Я думаю, что основная причина успеха вашего экспиримента скрывается в том, что вы используете одну и ту же платформу с одним и тем же компилятором.
При минимальном рассмотрении теория о том, что copy elision будет поддреживается в общем случае, представяется несостоятельной. Мои рассуждения, в результате которых я делаю такой вывод, следующие:
Во-первых,
Copy elision, как и любая другая опитимизация, описан в стандарте только своими эффектами (то что должно произойти с уровня абстракции языка), но стандарт не регламенитрует конкретную реализацию (то как имплементация конкретно должна это делать).
С этой точки зрения, два произвольно выбранных компилятора могут иметь разную реализацию. Да, количество путей, которыми можно сделать эту оптимизацию - строго ограничено, но в деталях она может отличаться. Эта разница в деталях могут сделать эту оптимизацию невозможной для двух разных модулей в общем случае. Ситуация получается аналогичной тому, как детали реализации механизма vtable
, рассмотренного в статье, приводят к тому же эффекту.
В частности,
Вызывающий функцию модуль, как и модуль, содержащий реализацию функции, всегда может быть собран со стандартом меньшим, чем С++17, например С++11/C++14. Поэтому компилятор, при сборке этого модуля, согласно стандарту С++11 и C++14, не обязан поддерживать copy elision.
Мой вывод:
Это implementation defined поведение, на которое я бы не стал завязываться, если вас интересует вопрос совместимости двух, потенциально собранных разными инструментами, модулей.
Поэтому, дорогой коллега @KanuTaH, если вы имеете хорошо проверенную информацию об обратном, или же вы готовы провести более подробное исследование, следующее более научной методологии, то я буду рад дополнить статью его результатами.
Сначала объясните как вообще возможно то о чём вы говорите. Функция не знает как её вызовут
Да нет никакой границы между модулями. Не су ще ству ет её. Только на винде из-за кривого динамического линкера. В остальных случаях - нет никакой границы, её невозможно пронаблюдать
Кажется это противоречит совету использовать unique_ptr
Чтобы прояснить ситуацию с умными указателями, пожалуйста, ознакомьтесь с пунктом 2.2 Конструирование объекта
P.P.S в статье раз 7 фигурирует std::move на const char*, видимо опечатка
Спасибо! Исправил.
А ещё в некоторых ситуациях мы просто не можем вернуть код ошибки, например из конструктора (технически можем, через аргумент ссылку, но это уже экзотика).
Экзотика-не экзотика, но вот на платформе, на которой работаю последние 6 лет это стандартный (на уровне системы) подход.
Есть понятие т.н. "структурированной ошибки"
typedef struct Qus_EC
{
int Bytes_Provided;
int Bytes_Available;
char Exception_Id[7];
char Reserved;
char Exception_Data[]; /* Varying length */
} Qus_EC_t;
суть которой в 7-значном коде ошибки + опциональном наборе данных (если таковые требуются). Т.е. условно говоря:
char error[sizeof(Qus_EC) + 50];
Qus_EC* pError = (Qus_EC*)error;
pError->Bytes_Provided = sizeof(error);
pError>Bytes_Available = 0;
Далее передаем pError куда нужно и на выходе проверяем
if (pError>Bytes_Available > 0); // где-то там была ошибка и структура заполнена
Сами сообщение хранятся в специальных message файлах (своего рода таблицы) где на каждый код сообщения хранится его тестовая расшифровка, уровень серьезности и т.п.
Например, код сообщения CPFAA01, текст "Attribute &1 for resource &2 type &3 not allowed to be monitored". Вместо &1, &2, &3 будут подставлены данные из блока Exception_Data (формат этого блока также хранится в message файле в виде количества возможных параметров и размера каждого из них).
Естественно, мы можем сам добавлять нужные нам сообщения в message файлы.
И, естественно, есть системная API, позволяющая по структуре Qus_EC получать полный текст ошибки.
Но в ситуации с переходом бинарной границы мы не можем использовать исключения, потому что способ их обработки зависит от ABI.
Справедливо только для исключений языка. А вот если механизм исключений поддерживается на уровне системы... то все становится намного проще. У каждого процесса (задания) в системе есть своя очередь сообщений. Куда можно помещать те же самые сообщения, что описаны выше. И этот механизм уже начинает работать не как возвращаемая ошибка, а как исключение. Которое на боится API т.к. идет в обход него. Помещение сообщения в очередь аналогично throw(), ну а catch уже сами размещаем где нам надо это исключение поймать. Хоть в этой программе, хоть в вызывающей ее... Например, можем сделать так:
/* Interrupt handler parameter block */
typedef _Packed struct {
unsigned int Block_Size; /* Size of the parameter block */
_INVFLAGS_T Tgt_Flags; /* Target invocation flags */
char reserved[8]; /* reserved */
_INVPTR Target; /* Current target invocation */
_INVPTR Source; /* Source invocation */
_SPCPTR Com_Area; /* Communications area */
char Compare_Data[32]; /* Compare Data */
char Msg_Id[7]; /* Message ID */
char reserved1; /* 1 byte pad */
_INTRPT_Mask_T Mask; /* Interrupt class mask */
unsigned int Msg_Ref_Key; /* Message reference key */
unsigned short Exception_Id; /* Exception ID */
unsigned short Compare_Data_Len; /* Length of Compare Data */
char Signal_Class; /* Internal signal class */
char Priority; /* Handler priority */
short Severity; /* Message severity */
char reserved3[4]; /* reserved */
int Msg_Data_Len; /* Len of available message data */
char Mch_Dep_Data[10]; /* Machine dependent date */
char Tgt_Inv_Type; /*Invocation type (in MIMCHOBS.H)*/
_SUSPENDPTR Tgt_Suspend; /* Suspend pointer of target */
char Ex_Data[48]; /* First 48 bytes of excp. data */
} _INTRPT_Hndlr_Parms_T;
static void ExeptHandler(_INTRPT_Hndlr_Parms_T* pexcp_data)
{
// в pexcp_data вся информация об исключении
// обрабатываем по своему усмотрению
}
// где знаем что может возникнуть исключение ставим
#pragma exception_handler(ExeptHandler, 0, _C1_ALL, _C2_ALL, _CTLA_HANDLE_NO_MSG)
В других языках на этой платформе синтаксис немного иной, но суть та же. Например
monitor;
// тут код, который может вызвать исключение
on-excp 'CPF501B';
// если вылетело исключение с кодом CPF501B - попадем сюда
on-error;
// если возникло другое исключение попадем сюда
end-mon;
Это уже полный аналог try-catch. Но в обход ABI независимо от языка на котором написано то, что вызвало исключение. Для извлечения сообщения об исключении из очереди также есть системная API.
Таким образом мы используем единый формат структурированной ошибки, которую можем вернуть как в качестве параметра, так и в виде исключения (при этом все исключения автоматически фиксируются в логе задания (joblog).
Очень интересный подход, спасибо, что поделились! И правда, если решать обработку ошибок на уровне системы, то проблем убудет. Это случайно не ОС под встроенную систему? От такого подхода с отказом от исключений на уровне системы немного веет программированием под встроенные системы.
В одной компании я увидел, как они реализовали свой механизм полиморфизма подтипов, в связи с чем они могли без зазрения совести делать вызовы "виртуальных" методов через бинарную границу. Но у этой компании и свой полноценный аналог STL был, со всякими дополнительными плюшками вида встроенной во фреймворк сериализации. Да и огромной она была, компания эта, деньги на разработку и необходимость в этих инструментах у них точно были.
Вцелом, я думаю, что такие подходы с переписыванием стандартных механизмов - очень дорогие, в связи с чем мало кто может себе такое позволить. И рассматривание таких механизмов - очень узкоспециализированная деятельность, далеко выходящая за рамки моего overview.
Это случайно не ОС под встроенную систему?
Даже близко не лежало :-) Это IBM i (IBM System i) "в девичестве" AS/400 (между собой ее так и называют - АС-ка).
Система нишевая - высоконадежные, высокопроизводительные коммерческие сервера на процессорах PowerS (у нас сейчас версия 7.4TR5 на процессорах Power9, самая свежая - 7.5 (не в курсе какой там последний TR для нее есть), самые свежие процессоры - Power10.
Это не мейнфреймы (которые у IBM тоже есть - IBM z и система AIX), они позиционируют платформу как middleware.
Очень специфичная, ни на что не похожая система, основана на принципе "все есть объект". Там даже файловая система принципиально другая - нет "файлов" - есть "объекты" (у каждого объекта есть имя, тип, атрибуты). Тот же файл сообщений, про который говорил выше - объект типа *MSGF. очередь сообщений (куда бросать исключения) - объект типа *PGMQ (есть еще просто очередь сообщений, не связанная с конкретным заданием - *MSGQ - туда тоже можно такие же сообщения кидать).
Сообщения (исключения) могут быть разных уровней -
*CMD Command
*COMP Completion
*DIAG Diagnostic
*ESCAPE Escape
*INFO Informational
*INQ Inquiry
*NOTIFY Notify
*RQS Request
*STATUS Status
Т.е. в целом это куда больше чем механизм исключений.
Если интересно, есть RedBook RPG: Exeption and Error Handling (RPG - специализированный язык для работы с БД и коммерческой логики, более 80% кода на этой платформе на нем пишется).
Вообще, это полностью "укомплектованная" самодостаточная система - покупая сервер вы сразу получаете все - установленную операционку с интегрированной (фактически это часть ОС) БД (DB2 for i), интегрированные в систему компиляторы языков - CL (Command Language, язык системных команд которые можно как в интерактиве для работы с системой, так и писать компилируемые программы на нем, можно свои команды создавать), COBOL (не знаю, пишет кто на нем сейчас или нет, но он есть), RPG (наиболее используемый язык тут), С/С++ (не сильно свежие, если не сказать сильно несвежие - вроде ка С++11 там сейчас)...
Еще одна замечательная вещь тут - концепция интегрированной языковой среды (ILE) - это уже вот прямо про ABI. Суть ее в том, что вы можете написать исходник на С/С++, исходник на RPG, исходник на CL, исходник на COBOL, потом каждый из них соотв. командой скомпилировать с "модуль" (объект типа *MODULE - функционально аналог объектного файла .obj в винде или .o в линуксе), а потом эти модули собрать (bind) в одну программу ("программный объект" - *PGM). И, если вы правильно опишите прототипы функций, то сможете вызывать из кода на RPG то, что написано, например, на C/С++.
Также из, например, RPG можно вызвать любую функцию Сишной библиотеки. Просто описать ее прототип со ссылкой на "внешнюю процедуру" и правильно описав параметры и тип возвращаемого значения.
Например, есть функция сишная:
struct timeval {
long tv_sec; /* second */
long tv_usec; /* microseconds */
};
struct timezone {
int tz_minuteswest; /* minutes west of Greenwich */
int tz_dsttime; /* daylight savings time flag */
};
int gettimeofday(struct timeval *, struct timezone *);
В RPG такого вот прямо нет, а вдруг потребовалось. Пишем:
//==============================================================================
// Текущее время в секундах с точностью до мкс
//==============================================================================
dcl-proc GetTime;
dcl-pi *n packed(16: 6);
end-pi;
dcl-ds t_dsTimeVal qualified template align(*full);
tv_sec int(10) inz;
tv_usec int(10) inz;
end-ds;
dcl-ds t_dsTimeZone qualified template align(*full);
tz_minuteswest int(10) inz;
tz_dsttime int(10) inz;
end-ds;
dcl-pr GetTimeofDay int(10) extproc(*CWIDEN : 'gettimeofday');
dsTime likeds(t_dsTimeVal);
dsZone likeds(t_dsTimeZone) options(*omit);
end-pr;
dcl-ds dsTimeVal likeds(t_dsTimeVal) inz(*likeds);
dcl-s pktTime packed(16: 6) inz(*zero);
dcl-s fltSecs float(8);
dcl-s fltUSecs float(8);
if GetTimeofDay(dsTimeVal: *omit) = 0;
fltSecs = dsTimeVal.tv_sec;
fltUSecs = dsTimeVal.tv_usec;
fltUSecs /= 1000000;
pktTime = fltSecs + fltUSecs;
endif;
return pktTime;
end-proc;
Для понимания -
packed(16: 6) - тип данных с фиксированной точкой, 16 знаков, 6 после запятой (кто знает SQL - 100% соответствует DECIMAL(16,6), в RPG вообще есть все типы данных которые есть в БД и SQL частности).
int(10) - соответствует int32 (для int16 будет int(5), int64 - int(20)
float(8) - double
dcl-pr GetTimeofDay int(10) extproc(*CWIDEN : 'gettimeofday'); - это как раз прототип функции со ссылкой на экспортируемую кем-то функцию gettimeofday
options(*omit) в описании параметра - значит что вместо него можно передать спецзначение *omit (по умолчанию параметры в RPG передаются по ссылке т.е. dsTime likeds(t_dsTimeVal) в RPG прототипе полностью соответствует struct timeval * в сишном прототипе. Передача *omit вместо параметра равнозначна передаче NULL. Еще может быть *nopass для одного или нескольких последних параметров, но С такого не поддерживает напрямую, хотя в RPG используется часто - есть даже проверка %passed(parmName) - передали этот параметр или нет. Для *omit, кстати, проверка %omited(parmName)
Т.о. вызов
GetTimeofDay(dsTimeVal: *omit);
фактически означает
gettimeofday(&dsTimeVal, NULL);
Правда, такой подход не очень часто используем. Ну разве что в программе на RPG нужен какой-то кусок (функция) которую проще и удобнее на С написать. А так, обычно, С/С++ код оформлется в отдельную сервисную программу (*SRVPGM - аналог dll в винде).
Вот, к примеру, есть у нас такой тип объекта - User Queue (*USRQ - очередь куда можно писать/читать произвольные данные, в т.ч. и с ключем). Очень полезный в хозяйстве объект, но работать с ним очень муторно, особенно из RPG (там вся работа через машинные инструкции - MI, в сишной библиотеке для них есть врапперы).
Пишем удобный для работы API на С/С++, собираем все это в сервисную программу. Пишем заголовочный файл с прототипами на RPG и пользуемся хоть из Сишных программ, хоть из RPGшных.
Например, в Сишном хидере
extern "C" int USRQ_Send(int hQueue, char* __ptr128 pBuffer, int nBuffLen,
char* __ptr128 pError);
extern "C" int USRQ_SendKey(int hQueue, char* __ptr128 pBuffer, int nBuffLen,
char* __ptr128 pKey, int nKeyLen, char* __ptr128 pError);
__ptr128 - небольшая специфика. Дело в том, что сервисная программа по ряду необходимостей использует модель памяти TERASPACE с 64бит указателем, которая позволяет выделять до 2Гб памяти одним куском, а RPGшные программы работают только в модели памяти SINGLE LEVEL - там указатели 128бит (в нем кроме указателя еще много всякой информации типа тегов защиты и т.п.), но одним куском выделить можно не более 16Мб. Так что здесь указывается что передаваемый указатель не 64, а 128бит.
В RPGшном оно же
// Отправка сообщения в queLIFO/queFIFO очередь
// Возвращает количество отправленных байт
// в случае ошибки -1
dcl-pr USRQ_SendMsg int(10) overload(USRQ_Send: USRQ_SendKey);
dcl-pr USRQ_Send int(10) extproc(*CWIDEN : 'USRQ_Send') ;
hQueue int(10) value; // handle объекта (возвращается USRQ_Connect)
pBuffer char(64000) options(*varsize); // Буфер для отправки
nBuffLen int(10) value; // Количество байт для отправки
Error char(37) options(*omit); // Ошибка
end-pr;
// Отправка сообщения в queKeyd очередь
// Возвращает количество отправленных байт
// в случае ошибки -1
dcl-pr USRQ_SendKey int(10) extproc(*CWIDEN : 'USRQ_SendKey') ;
hQueue int(10) value; // handle объекта (возвращается USRQ_Connect)
pBuffer char(64000) options(*varsize); // Буфер для отправки
nBuffLen int(10) value; // Количество байт для отправки
pKey char(256) const; // Значение ключа сообщения
nKeyLen int(10) value; // Фактический размер ключа
Error char(37) options(*omit); // Ошибка
end-pr;
value - параметр передается не по ссылке, а по значению
options(*varsize) - значит что реальный размер передаваемого параметра может быть любым, не обязательно 64000 байт (реальный размер передается отдельно). Система позволяет добавить в описание функции модификатор opdesc (а на стороне С добавляется специальная pragma) и тогда параметры будет передаваться в сочетании со специальными "операционными дескрипторами" и для каждого параметра специальной системной апишкой можно получить его тип и реальный размер (все есть объект - даже переменная в программе и система хранит свойства этого объекта и может передать их в вызываемую функцию в операционном дескрипторе). Но это несколько снижает производительность, а для нас производительность больное место, так что без крайней нужды стараемся не использовать такое.
Error char(37) - это та самая "структурированная ошибка". Правда, в RPG мы используем "усеченный вариант" - 7 символов код ошибки + 3 параметра по 10 символов. А особенность RPG в том, что структура (ds) тут трактуется как строка. Т.е. мы можем передавать как строку 37 символов, так и структуру из 4-х полей - char(7) для кода + 3 по char(10) для параметров.
dcl-pr USRQ_SendMsg int(10) overload(USRQ_Send: USRQ_SendKey); - да, RPG поддерживает overload для функций, возвращающих одинаковый тип данных, но с разным количеством и/или типом параметров. В коде пишем USRQ_SendMsg, а компилятор уже по набору параметров сам разбирается что реально подставить в вызов - USRQ_Send или USRQ_SendKey
Вот как-то так...
За 6 лет уже прикипел к этой системе настолько что уходить в нее никуда не хочется. Столько ту возможностей и настолько она внутренне цельная...
Вообще, есть "библия" - Френк Солтис. "Основы AS/400". Один из отцов-основателей этой системы.
Цитата от автора:
Менее года назад я был в Буэнос-Айресе на встрече с группой пользователей этой системы. По окончании встречи молодой репортер газеты «La Nacion» спросил меня: «Сформулируйте, пожалуйста, коротко причины того, почему в AS/400 столь много новшеств?». И я ответил: «Потому что никто из ее создателей не заканчивал MIT.»
Заинтригованный моим ответом, репортер попросил разъяснений. Я сказал, что не собирался критиковать MIT, а лишь имел в виду то, что разработчики AS/400 имели совершенно иной опыт, нежели выпускники MIT. Так как всегда было трудно заставить кого-либо переехать с восточного побережья в 70-тысячный миннесотский городок, в лаборатории IBM в Рочестере практически не оказалось выпускников университетов, расположенных на востоке США. И создатели AS/400 — представители «школ» Среднего Запада — не были так сильно привязаны к проектным решениям, используемым другими компаниями.
Т.е. создатели шли по своему пути и делали все без оглядки на то, как это реализовано у других. Согласитесь - работает вы в Windows или в Linux - внешне они похожи. Файлы, папки... Что в Windows, что в Linux у файла есть имя, есть расширение... И там и там вы можете открыть исполняемый файл HEX редактором и поправить там пару-тройку байтиков.
В АС-ке все не так. Есть объекты, у объекта есть имя (можно менять) каждый объект имеет свой тип и набор характерных для типа атрибутов (например, есть объект типа *FILE - это может быть "физический файл исходных текстов" - pf-src или "физический фал данных" - таблица БД - pf-dta или "логический файл" - некий аналог индекса, но в более широком смысле - lf). Вы не можете открыть редактором исполняемую программу - для объекта типа *PGM такая операция не определена в системе и следовательно невозможна.
Здесь нет папок, есть "библиотеки" (тоже объект - типа *LIB). Вместо переменной path тут есть Library List (*LIBL) - список библиотек где будет искаться запрашиваемый объект если явно не указана его библиотека. И вы в любой момент может добавить в либл библиотеку (ADDLIBLE) или удалить ее из либла (RMVLIBLE).
Ладно, понесло меня :-) Тут бесконечно можно рассказывать... Система очень богатая возможностями и реально необычная и интересная.
Выглядит так, что у вас достаточно материала, чтобы написать статью на эту тему :)
На самом деле нет. Даже после 6-ти лет работы с этой системой я не считаю что знаю ее достаточно глубоко.
Да и будет ли кому-то интересно? Система нишевая - банки, страховые и т.п. У нас используется мало где (Альфа, Росбанк, Райф - точно. Может быть в ПФР, если не ушли с нее, в РЖД были машинки такие, слышал).
Готовых разработчиков почти не найти - 2-3 сотни на страну, все "трудоустроены" и более-менее друг друга знают. Новых людей берем "с опытом разработки" и обучаем сами под эту систему. Но это не те люди, которые пришли, полгода-год поработали и ушли туда, где на три копейки больше пообещали. Тут весь опыт будет "нерелевантным". Много интересного, но за пределами неприменимого.
Если вы выбрасываете исключение из одного программного модуля и перехватываете и обрабатываете это исключение в другом программном модуле, то можете получить UB, поскольку каждый компилятор может реализовывать механзим исключений по-своему.
Подскажите пожалуйста, это тезис на основе практического опыта, или есть где про это почитать поподробнее? Я вероятно заблуждаюсь, но мне казалось что "C++ Exception Handling" описывается стандартом и компиляторы, соответствующие этому стандарту, должны поддерживать обработку исключений совместимым образом. То есть если не полагаться на специфические для компилятора расширения, то исключения должны выбрасываться и отлавливаться в соответствии со спецификациями стандарта, обеспечивая предсказуемое поведение.
Здравствуйте!
Я бы сказал, что мой личный опыт подтверждает теорию, которую я знаю. Действительно, сам механизм обработки исключений задекларирован в стандарте, но имплементация всё-ещё является платформо-зависимой, стандартного лайаута для С++ исключений, к сожалению не существует.
Но всё-ещё может вознакать проблема в разнице практического опыта, потому что доказать, что UB нет вцелом сложнее, чем доказать, что он есть. Мы можем видеть множество кейсов, в которых всё выглядит хорошо, но это не значит, что это не ведёт к UB.
Теперь, говоря о доказательной базе.
Например, есть спецификация Common Vendor ABI (Itanium C++ ABI), которую некоторые вендоры поддерживают в своих компиляторах.
Это попытка решить на уровне этой спецификации многие вопросы ABI совместимости, для которых стандарт оставил пространство для воображения.
По этому поводу даже был purposal WG21 N4028 Defining a Portable C++ ABI, но, судя по всему, он не был удовлетворён. В связи с чем, каждый компилятор горазд на свою имплементацию стандарта, в частности каждый сам решает хотят они поддерживать Common Vendor ABI (Itanium C++ ABI), или нет: GCC, к примеру, начиная с 3 версии, использует имплементацию Itanium:
... Furthermore, C++ source that is compiled into object files is transformed by the compiler: it arranges objects with specific alignment and in a particular layout, mangling names according to a well-defined algorithm, has specific arrangements for the support of virtual functions, etc. These details are defined as the compiler Application Binary Interface, or ABI. From GCC version 3 onwards the GNU C++ compiler uses an industry-standard C++ ABI, the Itanium C++ ABI.
Но жизнь в GCC есть и до его третьей версии, поэтому, даже при использовании только GCC, но разных его версий, мы можем увидеть странное поведение при переходе бинарной границы из-за разницы имплементаций.
Что конкретно может приводить к несовместимости?
Согласно Itanium Level II: C++ ABI 2.1 Introduction, стандарт С++ не настоял на следующих деталях:
The second level of specification is the minimum required to allow interoperability in the sense described above. This level requires agreement on:
Standard runtime initialization, e.g. pre-allocation of space for out-of-memory exceptions.
The layout of the exception object created by a throw and processed by a catch clause.
When and how the exception object is allocated and destroyed.
The API of the personality routine, i.e. the parameters passed to it, the logical actions it performs, and any results it returns (either function results to indicate success, failure, or continue, or changes in global or exception object state), for both the phase 1 handler search and the phase 2 cleanup/unwind.
How control is ultimately transferred back to the user program at a catch clause or other resumption point. That is, will the last personality routine transfer control directly to the user code resumption point, or will it return information to the runtime allowing the latter to do so?
Multithreading behavior.
The layout of the exception object created by a throw and processed by a catch clause.
When and how the exception object is allocated and destroyed.
The API of the personality routine, i.e. the parameters passed to it, the logical actions it performs, and any results it returns (either function results to indicate success, failure, or continue, or changes in global or exception object state), for both the phase 1 handler search and the phase 2 cleanup/unwind.
How control is ultimately transferred back to the user program at a catch clause or other resumption point. That is, will the last personality routine transfer control directly to the user code resumption point, or will it return information to the runtime allowing the latter to do so?
Multithreading behavior.
Разница может появиться из-за разницы механизмов:
Раскрутки стека
Когда вылатает исключение происходит раскрутка стека, до тех пор, пока не будет встречен первыйcatch
. Поскольку разные комиляторы могут использовать стек по разному, то огранизация стека может быть разной, что в свою очередь приведёт к тому, что во время обработки исключений два модуля, интерпретируя стек по разному, будут работать с его фреймами каждый по своим правилам.
Вероятные последствия: нарушение целостности системного стека, UB с иногда стреляющимSIGSEGV
/SIGBUS
.Создания и перехвата исключений
Механизм, используемый для создания и перехвата исключений (конструирвоание, копирование, перемещение, деструкция, выделение и освобождение памяти), может различаться в зависимости от компилятора.
Вероятные последствия: нарушение целостности памяти программы, UB с иногда стреляющимSIGSEGV
/SIGBUS
.Лайаута исключений
Расположение объектов исключений в памяти (порядок полей, выравнивание, и т.д.) может различаться в зависимости от компилятора.
Вероятные последствия: UB с иногда стреляющимSIGSEGV
/SIGBUS
.Менглинга имён
Поскольку ABI компиляторов может быть разным, они могут по-разному менглить и деменглить имена. У нас нет гарантий, наложенных С++ стандартом, что механизм менглинга имен будет одинаковым.
Вероятные последствия: в зависимости от имплементации, мы можем получить ODR violation и соответствующий UB.
Один из подвозных камней системного стека — его размер фиксирован и не изменяется со временем работы программы.
На современных платформах размер стека фиксирован только в смысле резервирования адресного пространства под него. Собственно же память под стек выделяется по мере его наполнения.
Процесс с 64-битным адресным пространством может позволить себе ну очень большие стеки. В той же Windows максимальный размер адресного пространства сейчас 128 TB, а физической памяти, даже в серверных версиях, всего 24 TB.
Важно понимать, что ОС нарочно делает вид, будто бы у неё есть бесконечное количество памяти
Не надо преувеличивать. Вовсе не бесконечное. На типичной Windows-системе с настройками по-умолчанию ограничение на commit порядка трёхкратного размера ОЗУ всего лишь. И всегда можно узнать сколько и чего осталось.
Мы, как программисты, обычно не соприкасаемся с реализацией таких вещей напрямую, поскольку они фигурируют только на затворках компиляции в недрах компилятора, и, к тому же, зависят от ОС.
Если не разбираться в таких вопросах, то зачем тогда вообще программировать на C\C++ ? Берите Java или там C# - толку больше будет.
Здравствуйте!
На современных платформах размер стека фиксирован только в смысле резервирования адресного пространства под него...
Подскажите, пожалуйста, как это отменяет исходное высказывание?
Размер стека всё ещё фиксирован, и бесконечное количество вызовов всё ещё приведёт к рекурсии.
Не надо преувеличивать. Вовсе не бесконечное.
Извините, я боюсь, что вы не прочитали этот пункт до конца. Про то что стек не бесконечный я рассказал в двух абзацах, следующих сразу за процитированным вами.
Размер стека всё ещё фиксирован, и бесконечное количество вызовов всё ещё приведёт к рекурсии.
Размер стека не фиксирован, а конечен, это все-таки разные вещи. Он безусловно может изменяться во время работы программы, просто не до бесконечности. Утверждение про невозможность бесконечной рекурсии правильное, а его обоснование - нет.
Размер стека не фиксирован, а конечен ..
.. [поэтому] Утверждение про невозможность бесконечной рекурсии правильное, а его обоснование - нет.
Извините, можете, пожалуйста, подтвердить свою точку зрения ссылкой на стандарт С++ (или спецификацию компилятора, вроде GCC/MSVC), где определена разница между понятиями конечности и фиксированности размера системного стека, и где приведена причинно-следственная связь этих понятий с переполнением стека из-за бесконечной рекурсии?
Насколько я знаю, это субективные понятия, которые, соответственно, могут интерпретироваться каждым по-разному.
Поэтому, дорогой коллега, прошу вас, пожалуйста, для подкрепления вашего субъективного мнения, привести ссылки на более объективные источники информации, вроде: стандарта С++, спецификации компиляторов, спецификации ABI (например Itanium), или же результатов исследований, следующих научной методологии.
Зачем вот эти кривляния уже не в первый раз? Разницу между понятиями конечности и фиксированности определяет не "стандарт C++", а толковый словарь.
Я прошу от вас более объективных заявлений, следующих научной методологии, подтверждённых чем-то кроме вашего личного мнения, потому что ваши замечания неконструктивны и содержат логические ошибки.
Субьективное мнение, к сожалению, не поможет в улучшении статьи и дополнении её объективными фактами. В связи с чем ваша мотивация выглядит сомнительной.
Почему ваши замечания неконструктивны?
Основываясь на выбрке ваших высказываний (см. внизу), многие ваши замечания содержат вашу субьективную оценку. В связи с чем, ваши замечания - не конструктивны.
https://habr.com/ru/articles/710658/#comment_26055918
Простите, а когда вы черпаете вот эти свои знания из советских газет
https://habr.com/ru/articles/710658/#comment_26055960
Простите, но это всего лишь ваше утверждение, ни на чем не основанное. Ну кроме фантазий со Stack Overflow.
https://habr.com/ru/articles/710658/#comment_26078678
Зачем вот эти кривляния уже не в первый раз
Почему ваши замечания содержат логические ошибки?
Вы допускаете логические ошибки следуя ненаучной методологии исследования, позволяя вашему субъективному мнению влиять на их результаты. Например, если взять ваш ответ:
Разницу между понятиями конечности и фиксированности определяет не "стандарт C++", а толковый словарь.
При минимамльном приближении можно понять, что толковый словарь, какой бы именно вы не имели ввиду (прим. толковый словарь Даля, Ожигова), не определяет разницу между конечностью и фиксированностью памяти системного стека. Понятия конечности и фиксированности памяти системного стека может определять только спецификация языка/компилятора/системы, в рамках которой этот стек и определён.
На современных платформах размер стека фиксирован
Ещё раз аккуратно перечитал абзац из статьи и ваше замечание. Я понял что вы имеете ввиду, проблема в том, что когда я говорю про фиксированность размера стека в контексте переполнения буффера, я на самом деле имею ввиду фиксированность его максимально допустимого размера.
Я исправил формулировку на более однозначную, благодарю за помощь!
Если не разбираться в таких вопросах, то зачем тогда вообще программировать на C\C++ ? Берите Java или там C# - толку больше будет.
Извините, но, субьективно, это высказывание мне не кажется конструктивным, особенно по отношению к новичкам.
Во-первых,
Чтобы его сделать его более объективным, вам нужно сопроводить своё высказывание градацией "важных внутренних механизмов языка С++", категоризированной по разным областям бизнеса, по которой будет видно важность каждого конкретного нюанса, и как знание о них влияет на бизнес. На вскидку, даже если бы я занялся такой категоризацией, субъективно, я бы не отнёс знание поднаготной ABI к мастхев знаниям для младшего разработчика. Кажется, что даже для множества разработчиков уровня middle и выше профит от этих знаний довольно эфимерный.
Во-вторых,
Я думаю, что, даже если это и возможно, выучить детали всех внутренних механизмов работы С++, то всё ещё невозможно постоянно держать их в голове и эффективно при этом работать. Потому что эти знания уходят корнями в бесчисленные нюансы работы ОС, о которых, в связи со сложностью устройства современных ОС, невозможно знать всё, заранее и сразу.
Поэтому,
Я думаю, что это хорошо для общего понимания - знать теорию, и как С++ работает "под капотом". Представление же о мире вида: "без знания всех этих нюансов у человека не получится написать ничего путного на С++", с моей точки зрения, не имеет ничего общего с реальностью.
Более того я считаю, что такой подход - деструктивен, он не ведёт ни к чему кроме разочаровния в бесконечной погоне за недостижимым "абсолютным знанием", в бесконечном цикле изучения "основных-основ" вкупе со штрудированем Александреску, The C Programming Language, и тонкостей языка Assembly.
Пардон, я опечатался:
Не надо преувеличивать. Вовсе не бесконечное.
Извините, я боюсь, что вы не прочитали этот пункт до конца. Про то что стек не бесконечный Про то, что количество динамической памяти - не бесконечно я рассказал в двух абзацах, следующих сразу за процитированным вами.
в таком параноидальном режиме совет может быть намного более простой -- на границе используется только си код
Здравствуйте! Наверное, вы имели ввиду, что нужно использовать Си API.
Я согласен, что если нужна гарантированная бинарная совместимость без лишних заморочек, то поддержка Си API может того стоить, и, к сожалению, это решение безальтернативное. Но, вцелом, как и у остальных решений, у подхода есть свои плюсы и минусы.
На самом деле есть ещё более простой совет - лучше не приводить к нарушению бинарной совместимости. Проще использовать header-only версию библиотеки, либо же пересобрать её под целевой тулсет, либо же найти уже пересобранную.
Я писал про это в выводе: "В одном чёрном-чёрном доме ..".
Из вывода
Насколько вы поняли, реальность ABI совместимости в C++ довольно сурова. Ещё более суровым её делает тот факт, что только при использовании Си API решение становится по-настоящему "бинарно-дружелюбным". Но, в то же время, со всем моим уважением к Си, код потеряет ту маленькую долю лакончиности и удобства, которое даёт нам С++, хотя, кажется, в тех редких случаях когда нам нужна 100% совместимость, другого выбора у нас нет.
Вы всегда можете написать Си API для своего юзкейса, перейти на использование POD типов, но на самом деле наилучший совет для перехода бинарной границы модулей с разным ABI — не переходить её. Чаще всего игра не стоит свеч (и в очередной раз отстреленных ног), поэтому проще использовать header-only
версию библиотеки, если она есть. Или же пересобрать библиотеку под свой тулсет, или найти уже пересобранную.
Простите за дилетантский вопрос, но когда мы пишем auto vector = std::vector (); мы, получается, переходим бинарную границу?
Уточню свой вопрос. Конечно, речь не непосредственно о векторе, я понимаю, что это шаблон и инстанцированный код класса будет частью моего объектного файла. Но сама реализация вектора, аллокации там и другая логика, могут перейти описываемую вами границу?
Если говорить коротко, то, да, рано или поздно управляющий поток перейдёт границу, чтобы обратиться к имплементации STL из libc++ (clang), libstdc++(gcc), либо же имплементации от msvc.
Вопрос ABI совместимости в STL был очень хорошо разобран в статье Binary Banshees and Digital Demons с перпективы автора многих proposal-ов в стандарт. К сожалению, я не видел версии этой статьи на русском, лично мне она кажется стоящей перевода.
Вольно переводя материал этой статьи и немного утрируя, можно сказать следующее:
Проблемы с менеджментом памяти и конструированием/деструкцией контейнеров можно решить при помощи std::polymorphic_allocator
, с его std::memory_resource
интерфейсом, который гарантирует ABI совместимость между разными версиями STL. Это значит, что если в вашей программе вы используете header из новой версии STL с более старым бинарём самой стандартной бибилиотеки (libc++ / libstdc++), то у вас гарантированно должно всё работать.
Но решение с std::memory_resource
проблемно, и суть проблема та же, что и у Си API. Дизайн решения - устаревший, и лучше уже не станет:
Дизайн
std::memory_resource
был зарелижен уже устаревшим, вдохновлённымstd::moneypunct
(дизайном локали из C++98-эры!).Лучше этот интерфейс уже точно не станет, потому что он связан с двух сторон: с одной стороны - требование полной ABI-совместимости, а с другой - его реализация на виртуальных функциях.
В резульате, улучшения стандартного аллокатора в новых версиях стандарта просто неприменимы дляstd::polymorphic_allocator
. Когда вstd::allocator
добавляютallocate_at_least
, то же самое уже не добавят вstd::polymorphic_allocator
, потому добавление новой фунции сломает ABI совместимость.
Глубина кроличьей норы: бинарная граница и ABI C++