Расширение временного времени жизни через элемент данных rvalue работает с агрегатом, но не с конструктором, почему?

Я нашел следующую схему для продления временного рабочего времени, я не знаю, нужно ли это делать, но это так.

struct S {
    std::vector<int>&& vec;
};

int main() {
    S s1{std::vector<int>(5)};      // construct with temporary
    std::cout << s1.vec[0] << '\n'; // fine, temporary is alive
}

Однако, когда S задан явный конструктор значений, он больше не является агрегатом, и эта схема выходит из строя с недопустимым чтением на s1.vec[0]

struct S {
    std::vector<int>&& vec;
    S(std::vector<int>&& v)
        : vec{std::move(v)}         // bind to the temporary provided
    { }
};

int main() {
    S s1{std::vector<int>(5)};      // construct with temporary
    std::cout << s1.vec[0] << '\n'; // not ok. invalid read on free'd memory
}

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

Есть много вопросов, касающихся аналогичной ситуации, используя ссылки lvalue на SO. Я вижу, что если бы я использовал const lvalue ref, это не помогло бы продлить время жизни временного, будут ли правила для rvalue refs одинаковыми?

Ответ 1

TL; DR

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

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



Что такое агрегат?

struct S {                // (1)
  std::vector<int>&& vec;
};

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

8.5.1p1 Агрегаты [dcl.init.aggr]

Агрегат - это массив или класс (раздел 9) без конструкторов, предоставляемых пользователем (12.1), без частных или защищенных нестатических элементов данных (раздел 11), без базовых классов (раздел 10) и без виртуальных функции (10.3)

Примечание. Вышеупомянутое означает, что (1) является агрегатом.



Как инициализируются агрегаты?

Инициализация между агрегатом и "неагрегатом" сильно отличается, здесь идет другой раздел прямо из стандарта:

8.5.1p2 Агрегаты [dcl.init.aggr]

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


В приведенной выше цитате указано, что мы инициализируем членов нашего агрегата с инициализаторами в предложении initializer, между ними нет шага.

struct A { std::string a; int b; };

A x { std::string {"abc"}, 2 };


Семантически приведенное выше эквивалентно инициализации наших членов с использованием приведенного ниже, просто A::a и A::b в этом случае доступны только через x.a и x.b.

std::string A::a { std::string {"abc"} };
int         A::b { 2 };


Если мы изменим тип A::a на rvalue-reference или const lvalue-reference, мы напрямую привяжем временное использование для инициализации к x.a.

Правила rvalue-reference и const lvalue-ссылки говорят, что время жизни будет распространено на время жизни хоста, что и должно произойти.



Как инициализация с использованием конструктора, объявленного пользователем?

struct S {                    // (2)
    std::vector<int>&& vec;
    S(std::vector<int>&& v)
        : vec{std::move(v)}   // bind to the temporary provided
    { }
};

Конструктор - это не что иное, как причудливая функция, используемая для инициализации экземпляра класса. К ним применяются те же правила, которые применяются к функциям.

Когда дело доходит до продления срока жизни временных разниц, нет никакой разницы.

std::string&& func (std::string&& ref) {
  return std::move (ref);
}


Временный, переданный в func, не будет продлевать срок его службы только потому, что мы имеем аргумент, объявленный как rvalue/lvalue-reference. Даже если мы вернем "ту же" ссылку, чтобы она была доступна вне func, этого просто не произойдет.

Это то, что происходит в конструкторе (2), ведь конструктор - это просто "причудливая функция", используемая для инициализации объекта.


12.2p5 Временные объекты [class.temporary]

Временная привязка ссылки или временный объект, являющийся полным объектом подобъекта, к которому привязана ссылка, сохраняется для времени жизни ссылки, за исключением:

     
  •   
  • Временная привязка к ссылочному элементу в конструкторе ctor-initializer (12.6.2) сохраняется до завершения конструктора.

      
  • Временная привязка к ссылочному параметру в вызове функции (5.2.2) сохраняется до завершения полного выражения, содержащего вызов.

      
  • Время жизни временной привязки к возвращаемому значению в операторе return функции (6.6.3) не распространяется; временное уничтожается в конце полного выражения в операторе return.

         
    •   
    • Временная привязка к ссылке в new-initializer (5.3.4) сохраняется до завершения полного выражения, содержащего новый-инициализатор.  
      

Примечание. Обратите внимание, что агрегатная инициализация с помощью new T { ... } отличается от ранее упомянутых правил.