Вложенный 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 конструктора".

Другие вопросы по тегам