Подтвердить что ты не робот

Время жизни временных объектов во время инициализации списка

Я всегда предполагал, что временные объекты живут до конца полного выражения. Здесь, однако, любопытная разница между инициализацией a std::vector и массивом.

Обратите внимание на следующий код:

#include <iostream>
#include <vector>

struct ID{ 
  static int cnt;
  // the number of living object of class ID at the moment of creation:  
  int id;

  ID():id(++cnt){}

  ~ID(){
     cnt--;
  }
};

int ID::cnt=0;

int main(){

  int arr[]{ID().id, ID().id};
  std::vector<int> vec{ID().id, ID().id};

  std::cout<<" Array: "<<arr[0]<<", "<<arr[1]<<"\n";
  std::cout<<" Vector: "<<vec[0]<<", "<<vec[1]<<"\n";
}

Результат этой программы немного (по крайней мере для меня) неожиданно:

 Array: 1, 1
 Vector: 1, 2

Это означает, что временные объекты являются живыми во время всей инициализации std::vector, но они создаются и разрушаются один за другим в случае массива. Я ожидал бы, что временные данные будут жить до тех пор, пока полное выражение int arr[]{ID().id, ID().id}; не будет завершено.

В стандарте упоминается одно исключение в отношении времени жизни временных объектов и инициализации массивов (12.2). Однако я не понимаю его смысла и не знаю, почему он применяется в этом конкретном случае:

Существует два контекста, в которых временные другая точка, чем конец полного выражения. Первый контекст когда вызывается конструктор по умолчанию для инициализации элемента массив. Если конструктор имеет один или несколько аргументов по умолчанию, уничтожение каждого временного объекта, созданного в аргументе по умолчанию, секвентированный до построения следующего элемента массива, если он есть.


Обзор результатов с разными компиляторами (результат MSVS является репрезентацией NathanOliver):

             Array    Vector
clang 3.8    1, 2      1, 2
g++ 6.1      1, 1      1, 2
icpc 16      1, 1      1, 2
MSVS 2015    1, 1      1, 2

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

  struct S{
      int a;
      int b;
  } s{ID().id, ID().id};
  std::cout<<" Struct: "<<s.a<<", "<<s.b<<"\n";

должен печатать Struct 1, 1 на консоли. Это именно то, что делает программа, скомпилированная g++. Тем не менее, у clang, похоже, есть ошибка - итоговая программа выводит Struct 1, 2.


Сообщалось об ошибке: https://llvm.org/bugs/show_bug.cgi?id=29080

4b9b3361

Ответ 1

Это основная проблема 1343 "Последовательность инициализации без класса" , которая была принята в качестве отчета о дефектах в ноябре 2016 года на бумаге P0570R0. Предлагаемая резолюция является частью С++ 17, но не является частью С++ 14, поэтому (если комитет не решит опубликовать исправление к С++ 14), это точка различия между С++ 17 и C + +14.

С++ 14

Правильный вывод в соответствии с правилами стандарта С++ 14 составляет 1, 1 для массива и 1, 2 для вектора; это связано с тем, что для построения вектора (в том числе из списка с привязкой-инициализацией) требуется вызов конструктора при построении массива.

Язык, который регулирует это, находится в [intro.execution]:

10 - Полное выражение - это выражение, которое не является подвыражением другого выражения. [...] Если языковая конструкция определена для получения неявного вызова функции, использование конструкции языка считается выражением для целей этого определения. [...]

Это хорошо, как обзор верхнего уровня, но он оставляет неотвеченные некоторые вопросы:

  • Именно эта конструкция языка считается конструкцией, производящей неявный вызов функции;
  • Что на самом деле считается неявным вызовом функции; предположительно вызов определенного пользователем конструктора является вызовом функции, но как насчет конструктора, который по умолчанию установлен или определен как дефолт?

Массив - это совокупность, поэтому инициализируется из списка с привязкой-init-init в соответствии с [dcl.init.aggr]; это говорит о том, что каждый элемент инициализируется непосредственно из соответствующего элемента списка, поэтому не существует неявного вызова функции (по крайней мере, не соответствует общей инициализации). На уровне синтаксиса в инициализаторе ( [dcl.init]/1), используя список с привязкой-init-списком в качестве элемента для выравнивания или равносильности, полные выражения являются выражениями, содержащимися в скобки и разделенные запятыми. В конце каждого полного выражения деструкторы временных рядов должны выполняться, так как здесь не существует ни одного из трех контекстов, упомянутых в [class.temporary].

Случай для инициализации вектора отличается, поскольку вы используете конструктор initializer_list, поэтому возникает неявный вызов функции (т.е. конструктор initializer_list); это означает, что существует неявное полное выражение, окружающее всю инициализацию, поэтому временные файлы уничтожаются только тогда, когда завершается инициализация вектора.

Смутно, [dcl.init.list] говорит, что ваш код "примерно эквивалентен":

const int __a[2] = {int{ID().id}, int{ID().id}};  // #1
std::vector<int> vec(std::initializer_list<int>(__a, __a + 2));

Однако это нужно читать в контексте - например, массив, поддерживающий initializer_list, имеет время жизни, ограниченное инициализацией вектора.

Это было намного яснее в С++ 03, который был в [intro.execution]:

13 - [Примечание: некоторые контексты в С++ вызывают оценку полного выражения, которое получается из синтаксической конструкции кроме выражения (5.18). Например, в 8.5 один синтаксис для инициализатора ( expression-list )но результирующая конструкция представляет собой вызов функции функции-конструктора с выражением-списком в качестве аргумента список; такой вызов функции является полным выражением. Например, в 8.5 другой синтаксис для инициализатора = initializer-clauseно снова полученная конструкция может быть вызовом функции для функции-конструктора с одним присваиванием-выражением как аргумент; снова вызов функции является полным выражением. ]

Этот абзац полностью поражен С++ 11; это соответствовало разрешению CWG 392. В результате путаница, по-видимому, не была предназначена.

С++ 17

После P0570R0 [intro.execution] заявляет, что полное выражение: [...]

  • init-declarator ([dcl.decl]) [...], включая составляющие выражения инициализатора, или [...]
  • выражение, которое не является подвыражением другого выражения и которое не является частью полного выражения.

Итак, в С++ 17 полное выражение arr[]{ID().id, ID().id} и vec{ID().id, ID().id} соответственно, а правильный вывод 1, 2 в каждом случае, так как уничтожение первого временного ID откладывается до конец полного выражения.