Вложенный std::forward_as_tuple и ошибка сегментации
Моя настоящая проблема намного сложнее, и, кажется, чрезвычайно сложно привести короткий конкретный пример, чтобы воспроизвести ее. Поэтому я публикую здесь другой небольшой пример, который может быть актуален, и его обсуждение также может помочь в решении актуальной проблемы:
// A: works fine (prints '2')
cout << std::get <0>(std::get <1>(
std::forward_as_tuple(3, std::forward_as_tuple(2, 0)))
) << endl;
// B: fine in Clang, segmentation fault in GCC with -Os
auto x = std::forward_as_tuple(3, std::forward_as_tuple(2, 0));
cout << std::get <0>(std::get <1>(x)) << endl;
Актуальная проблема не связана с std::tuple
Итак, чтобы сделать пример независимым, вот пользовательский, минимальный грубый эквивалент:
template <typename A, typename B>
struct node { A a; B b; };
template <typename... A>
node <A&&...> make(A&&... a)
{
return node <A&&...>{std::forward <A>(a)...};
}
template <typename N>
auto fst(N&& n)
-> decltype((std::forward <N>(n).a))
{ return std::forward <N>(n).a; }
template <typename N>
auto snd(N&& n)
-> decltype((std::forward <N>(n).b))
{ return std::forward <N>(n).b; }
Учитывая эти определения, я получаю точно такое же поведение:
// A: works fine (prints '2')
cout << fst(snd(make(3, make(2, 0)))) << endl;
// B: fine in Clang, segmentation fault in GCC with -Os
auto z = make(3, make(2, 0));
cout << fst(snd(z)) << endl;
В целом, похоже, что поведение зависит от компилятора и уровня оптимизации. Я не смог ничего выяснить путем отладки. Похоже, что во всех случаях все встроено и оптимизировано, поэтому я не могу определить конкретную строку кода, вызывающую проблему.
Если предполагается, что временные файлы будут существовать до тех пор, пока на них есть ссылки (а я не возвращаю ссылки на локальные переменные из тела функции), я не вижу какой-либо фундаментальной причины, по которой приведенный выше код может вызвать проблемы и почему случаи A и Б должен отличаться.
В моей реальной проблеме и Clang, и GCC выдают ошибки сегментации даже для однострочных версий (случай A) и независимо от уровня оптимизации, поэтому проблема довольно серьезная.
Проблема исчезает при использовании значений или ссылок на значения (например, std::make_tuple
, или же node <A...>
в кастомной версии). Это также исчезает, когда кортежи не являются вложенными.
Но ничего из вышеперечисленного не помогает. То, что я реализую, - это своего рода шаблоны выражений для представлений и ленивых вычислений для ряда структур, включая кортежи, последовательности и комбинации. Поэтому мне определенно нужны ссылки на временные ссылки. Все отлично работает для вложенных кортежей, например (a, (b, c))
для выражений с вложенными операциями, например u + 2 * v
, но не оба.
Я был бы признателен за любые комментарии, которые помогли бы понять, является ли приведенный выше код корректным, ожидается ли ошибка сегментации, как я могу ее избежать и что может происходить с компиляторами и уровнями оптимизации.
1 ответ
Проблема здесь заключается в утверждении "Если временные люди должны жить так долго, как есть ссылки на них". Это верно только в ограниченных обстоятельствах, ваша программа не является демонстрацией одного из этих случаев. Вы храните кортеж, содержащий ссылки на временные файлы, которые уничтожаются в конце полного выражения. Эта программа демонстрирует это очень четко ( живой код в Coliru):
struct foo {
int value;
foo(int v) : value(v) {
std::cout << "foo(" << value << ")\n" << std::flush;
}
~foo() {
std::cout << "~foo(" << value << ")\n" << std::flush;
}
foo(const foo&) = delete;
foo& operator = (const foo&) = delete;
friend std::ostream& operator << (std::ostream& os,
const foo& f) {
os << f.value;
return os;
}
};
template <typename A, typename B>
struct node { A a; B b; };
template <typename... A>
node <A&&...> make(A&&... a)
{
return node <A&&...>{std::forward <A>(a)...};
}
template <typename N>
auto fst(N&& n)
-> decltype((std::forward <N>(n).a))
{ return std::forward <N>(n).a; }
template <typename N>
auto snd(N&& n)
-> decltype((std::forward <N>(n).b))
{ return std::forward <N>(n).b; }
int main() {
using namespace std;
// A: works fine (prints '2')
cout << fst(snd(make(foo(3), make(foo(2), foo(0))))) << endl;
// B: fine in Clang, segmentation fault in GCC with -Os
auto z = make(foo(3), make(foo(2), foo(0)));
cout << "referencing: " << flush;
cout << fst(snd(z)) << endl;
}
A
работает нормально, потому что он обращается к ссылкам, хранящимся в кортеже в том же полном выражении, B
имеет неопределенное поведение, так как хранит кортеж и обращается к ссылкам позже. Обратите внимание, что, хотя он может не потерпеть крах при компиляции с помощью clang, его поведение, тем не менее, явно неопределенное из-за доступа к объекту после окончания его срока службы.
Если вы хотите сделать это использование безопасным, вы можете довольно легко изменить программу, чтобы хранить ссылки на lvalues, но переместить rvalues в сам кортеж ( живая демонстрация в Coliru):
template <typename... A>
node<A...> make(A&&... a)
{
return node<A...>{std::forward <A>(a)...};
}
Замена node<A&&...>
с node<A...>
это хитрость: так как A
универсальная ссылка, фактический тип A
будет ссылкой на lvalue для аргументов lvalue и не ссылочным типом для аргументов rvalue. Правила свертывания ссылок работают в нашу пользу для этого использования, а также для идеальной пересылки.
РЕДАКТИРОВАТЬ: Что касается того, почему временные в этом сценарии не были продлены время жизни до времени жизни ссылок, мы должны взглянуть на C++11 12.2 Временные объекты [class.teilitary] параграф 4:
Существует два контекста, в которых временные объекты уничтожаются в другой точке, чем конец полного выражения. Первый контекст - это когда конструктор по умолчанию вызывается для инициализации элемента массива. Если конструктор имеет один или несколько аргументов по умолчанию, уничтожение каждого временного объекта, созданного в аргументе по умолчанию, выполняется до создания следующего элемента массива, если таковой имеется.
и гораздо более сложный пункт 5:
Второй контекст, когда ссылка связана с временным. Временный объект, к которому привязана ссылка, или временный объект, являющийся полным объектом подобъекта, к которому привязана ссылка, сохраняется в течение всего времени существования ссылки, за исключением:
Временная привязка к элементу ссылки в ctor-initializer конструктора (12.6.2) сохраняется до выхода из конструктора.
Временная привязка к ссылочному параметру в вызове функции (5.2.2) сохраняется до завершения полного выражения, содержащего вызов.
Время жизни временной привязки к возвращенному значению в операторе возврата функции (6.6.3) не продлевается; временное уничтожается в конце полного выражения в операторе возврата.
Временная привязка к ссылке в новом инициализаторе (5.3.4) сохраняется до завершения полного выражения, содержащего новый инициализатор. [ Пример:
struct S { int mi; const std::pair<int,int>& mp; };
S a { 1, {2,3} };
S* p = new S{ 1, {2,3} }; // Creates dangling reference
- конец примера] [Примечание: это может привести к появлению висячей ссылки, и реализации рекомендуется в этом случае выдавать предупреждение. —Конечная записка]
Уничтожение временного, чье время жизни не продлевается путем привязки к ссылке, упорядочивается до уничтожения каждого временного объекта, созданного ранее в том же полном выражении. Если время жизни двух или более временных объектов, к которым привязаны ссылки, заканчивается в одной и той же точке, эти временные объекты уничтожаются в этой точке в порядке, обратном завершению их построения. Кроме того, при уничтожении временных ссылок, связанных с ссылками, следует учитывать порядок уничтожения объектов со статическим, потоковым или автоматическим сроком хранения (3.7.1, 3.7.2, 3.7.3); то есть если
obj1
является объектом с той же продолжительностью хранения, что и временное, и созданным до создания временного, временное должно быть уничтожено доobj1
уничтожен; еслиobj2
является объектом с той же продолжительностью хранения, что и временный и созданный после создания временного, временный должен быть уничтожен послеobj2
уничтожен [ Пример:struct S { S(); S(int); friend S operator+(const S&, const S&); ~S(); }; S obj1; const S& cr = S(16)+S(23); S obj2;
выражение
S(16) + S(23)
создает три временных: первый временныйT1
держать результат выраженияS(16)
, второй временныйT2
провести результат выражения S(23) и третий временныйT3
провести результат сложения этих двух выражений. ВременныйT3
затем привязывается к ссылкеcr
, Не указано,T1
или жеT2
создается первым. На реализации гдеT1
создан раньшеT2
Гарантируется, чтоT2
уничтожен раньшеT1
, ВременныеT1
а такжеT2
привязаны к эталонным параметрамoperator+
; эти временные символы уничтожаются в конце полного выражения, содержащего вызовoperator+
, ВременныйT3
привязан к ссылкеcr
уничтожен в концеcr
Это время жизни, то есть в конце программы. Кроме того, порядок, в которомT3
Уничтожается с учетом порядка уничтожения других объектов со статической продолжительностью хранения. Это потомуobj1
построен раньшеT3
, а такжеT3
построен раньшеobj2
Гарантируется, чтоobj2
разрушен до T3, и этоT3
уничтожен раньшеobj1
, - конец примера]
Вы привязываете временное "к элементу ссылки в ctor-initializer конструктора".