Массив несмежных объектов

#include <iostream> 
#include <cstring>
// This struct is not guaranteed to occupy contiguous storage
// in the sense of the C++ Object model (§1.8.5):
struct separated { 
  int i; 
  separated(int a, int b){i=a; i2=b;} 
  ~separated(){i=i2=-1;} // nontrivial destructor --> not trivially   copyable
  private: int i2;       // different access control --> not standard layout
};
int main() {
  static_assert(not std::is_standard_layout<separated>::value,"sl");
  static_assert(not std::is_trivial<separated>::value,"tr");
  separated a[2]={{1,2},{3,4}};
  std::memset(&a[0],0,sizeof(a[0]));
  std::cout<<a[1].i;    
  // No guarantee that the previous line outputs 3.
}
// compiled with Debian clang version 3.5.0-10, C++14-standard 
// (outputs 3) 
  1. В чем причина ослабления стандартных гарантий до такой степени, что эта программа может демонстрировать неопределенное поведение?

  2. Стандарт гласит: «Объект типа массива содержит непрерывно выделенный непустой набор из N подобъектов типа T». [dcl.массив] §8.3.4. Если объекты типа T не занимают непрерывную память, как это может сделать массив таких объектов?

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


person Heiko Bloch    schedule 30.09.2016    source источник
comment
Что вы имеете в виду, что объект не занимает непрерывное хранилище? Вы говорите о дополнении, которое может быть между переменными-членами?   -  person NathanOliver    schedule 30.09.2016
comment
Математика говорит, что запрос «разреженные массивы» в google может вам помочь??? , извините, мой английский слишком плохой, чтобы помочь глубже.   -  person Jacek Cz    schedule 30.09.2016
comment
На ваш первый вопрос: потому что никто не хочет проектировать C++ вокруг C, таких как memset. Структуры C должны работать с memset для совместимости, остальное не имеет большого значения.   -  person Baum mit Augen    schedule 30.09.2016
comment
Откуда это? Вы запустили его и не получили 3? Есть комментарий, в котором говорится, что нет гарантии, что ... но я не знаю, кто это утверждает.   -  person Kenny Ostrom    schedule 30.09.2016
comment
Просто стандарт не утверждает однозначно, что программа выводит 3. Даже если бы не было проблем с memset (можно было бы использовать размещение только массивов new и char), два целых числа могли бы находиться в разных адресных пространствах. (последовательности смежных байтов на стандартном языке)   -  person Heiko Bloch    schedule 30.09.2016
comment
Ваша логика ошибочна: тривиально копируемая или стандартная компоновка подразумевает непрерывные байты памяти. 1.8.5 не запрещает постоянное размещение в памяти типов с нестандартной компоновкой.   -  person knivil    schedule 30.09.2016
comment
@knivil: я не говорил: это запрещено. Я сказал: это не гарантируется.   -  person Heiko Bloch    schedule 30.09.2016
comment
Вызов memset() в вашем примере устанавливает только самое первое целое число в массиве равным 0. Если вы хотите обнулить весь массив, вы должны использовать memset(a, 0, sizeof a).   -  person G. Sliepen    schedule 01.10.2016
comment
Но я намеревался обнулить только [0]. Вопрос в следующем: если я сделаю это таким образом, могу ли я непреднамеренно записать в a[1] на машинах, которые немного более экзотические, чем настольный ПК?   -  person Heiko Bloch    schedule 01.10.2016
comment
Смысл в том, что стандарт не хочет ограничивать реализацию сложных классов.   -  person M.M    schedule 02.10.2016
comment
@JoachimPileborg стандарт разрешает части хранилища, необходимые для реализации объекта, находиться в совершенно отдельных областях памяти (например, vtables)   -  person M.M    schedule 02.10.2016
comment
Помимо несмежности объектов, есть много веских причин, почему memsetting сложный объект должен быть UB.   -  person n. 1.8e9-where's-my-share m.    schedule 04.10.2016


Ответы (1)


1. Это пример бритвы Оккама, принятой драконами, которые на самом деле пишут компиляторы: не давайте больше гарантий, чем необходимо для решения проблемы, потому что иначе ваша рабочая нагрузка удвоится без компенсации. Сложные классы, адаптированные к модному оборудованию или к историческому оборудованию, были частью проблемы. (намекает Бауммит Оген и М.М.)

2. (смежные = имеющие общую границу, рядом или вместе в последовательности)

Во-первых, это не значит, что объекты типа T всегда или никогда не занимают непрерывную память. В одном двоичном файле могут быть разные макеты памяти для одного и того же типа.

[class.derived] §10 (8): Подобъект базового класса может иметь макет, отличный от...

Этого было бы достаточно, чтобы откинуться назад и убедиться, что происходящее на наших компьютерах не противоречит стандарту. Но давайте изменим вопрос. Правильнее было бы спросить:

Разрешает ли стандарт массивы объектов, которые не занимают непрерывное хранилище по отдельности, и в то же время каждые два последовательных подобъекта имеют общую границу?

Если это так, то это сильно повлияет на то, как char* арифметика соотносится с T* арифметикой.

В зависимости от того, понимаете ли вы стандартную цитату OP, означающую, что только подобъекты имеют общую границу, или что внутри каждого подобъекта байты имеют общую границу, вы можете прийти к разным выводам.

Предполагая первое, вы обнаружите, что «непрерывно распределенные» или «хранящиеся непрерывно» могут просто означать &a[n]==&a[0] + n (§23.3.2.1), что является утверждением об адресах подобъектов, которое не подразумевало бы, что массив находится в одной последовательности смежных байтов.

Если вы предполагаете более сильную версию, вы можете прийти к выводу 'element offset==sizeof(T)', выдвинутому в T* против арифметики указателей char* Это также подразумевало бы, что можно заставить в противном случае, возможно, несмежные объекты в непрерывную компоновку, объявив их T t[1]; вместо Т т;

Как теперь решить этот бардак? В стандарте есть принципиально неоднозначное определение оператора sizeof(), которое кажется пережитком того времени, когда, по крайней мере, для каждой архитектуры тип примерно равнялся макету, чего сейчас нет. (Как новое размещение узнает, какой макет создать?)

Применительно к классу результатом [of sizeof()] является количество байтов в объекте этого класса, включая любое заполнение, необходимое для размещения объектов этого типа в массиве. [expr.sizeof] §5.3.3 (2)

Но подождите, количество требуемого отступа зависит от макета, а один тип может иметь более одного макета. Таким образом, мы обязаны добавить немного соли и взять минимум из всех возможных макетов или сделать что-то столь же произвольное.

Наконец, определение массива выиграет от устранения неоднозначности с точки зрения char* арифметики, если это предполагаемое значение. В противном случае применяется ответ на вопрос 1 соответственно.


Несколько замечаний, связанных с уже удаленными ответами и комментариями: noredirect=1&lq=1">Могут ли технически объекты занимать несмежные байты памяти?, несмежные объекты действительно существуют. Кроме того, наивное запоминание подобъекта может сделать недействительными несвязанные подобъекты содержащего объекта, даже для совершенно смежных, тривиально копируемых объектов:

#include <iostream>
#include <cstring>
struct A {
  private: int a;
  public: short i;
};
struct B :  A {
  short i;
};
int main()
{
   static_assert(std::is_trivial<A>::value , "A not trivial.");
   static_assert(not std::is_standard_layout<A>::value , "sl.");
   static_assert(std::is_trivial<B>::value , "B not trivial.");
   B object;
   object.i=1;
   std::cout<< object.B::i;
   std::memset((void*)&(A&)object ,0,sizeof(A));
   std::cout<<object.B::i;
}
// outputs 10 with g++/clang++, c++11, Debian 8, amd64     

Следовательно, вполне возможно, что memset в сообщении с вопросом может обнулить a[1].i, так что программа выведет 0 вместо 3.

Есть несколько случаев, когда можно вообще использовать memset-подобные функции с объектами C++. (Обычно деструкторы подобъектов явно не работают, если вы это сделаете.) Но иногда нужно очистить содержимое класса 'почти-POD' в его деструкторе, и это может быть исключением.

person Heiko Bloch    schedule 05.10.2016
comment
Поскольку можно поместить объект в соответствующим образом выровненный массив символов соответствующего размера, кажется, что да, по крайней мере, можно заставить в противном случае, возможно, несмежные объекты в непрерывной компоновке, независимо от интерпретации арифметики указателя. - person n. 1.8e9-where's-my-share m.; 10.10.2016
comment
Кроме того, поскольку можно вручную вызвать деструктор, а затем принудительно поместить новый объект в уже пустое место хранения, кажется, что у реализации нет другого выбора, кроме как использовать один и тот же непрерывный макет для всех наиболее производных объектов одного и того же типа. - person n. 1.8e9-where's-my-share m.; 10.10.2016
comment
@н.м. Я предполагаю, что вы имеете в виду новое размещение, но несмежный макет остается, могут быть части объекта, которые не помещаются в буфер. Виртуальная таблица является распространенным примером этого. - person M.M; 11.10.2016
comment
Как один тип может иметь несколько макетов (на одном компиляторе+ОС+архитектуре)? - person rubenvb; 12.10.2016
comment
@MMA vtable ни в коем случае не является частью объекта. Обычно он существует до создания объекта и после его уничтожения и используется многими объектами одного типа. Если вы называете vtable частью объекта, вызовите функцию как часть указателя на функцию. - person n. 1.8e9-where's-my-share m.; 12.10.2016
comment
@н.м. тем не менее, именно это имеется в виду, когда в стандарте говорится, что объекты нестандартной компоновки не могут занимать непрерывную память. - person M.M; 12.10.2016
comment
@M.M Правда? Нужна цитата. - person n. 1.8e9-where's-my-share m.; 12.10.2016
comment
@н.м. У меня нет цитаты, но я видел обсуждение темы раньше. - person M.M; 12.10.2016
comment
@ M.M Я считаю, что некоторые люди действительно рассматривают vtable как часть объекта. Это не имеет никакого смысла для меня. Если это было целью стандарта, лежащего в основе рассматриваемой формулировки, то, по мнению ИМО, аргументация была ошибочной, и формулировку необходимо пересмотреть. Другая причина этого заключается в том, что vtable не имеет размера в том, что касается sizeof, поэтому нет причин рассматривать ее как занимающую память. - person n. 1.8e9-where's-my-share m.; 12.10.2016
comment
@н.м. Хранилище, охватываемое sizeof, относится к непрерывному хранилищу, начиная с первого байта объекта, поэтому оно не может быть тем, что подразумевается под несмежным хранилищем. - person M.M; 12.10.2016
comment
@М.М. Память, охватываемая sizeof, относится к непрерывной памяти, начиная с первого байта объекта. В стандарте ничего подобного не сказано. Оператор sizeof возвращает число байтов в объектном представлении его операнда. Вот и все. - person n. 1.8e9-where's-my-share m.; 12.10.2016
comment
@н.м. ОК [заполнение] - person M.M; 12.10.2016