Почему C и C ++ поддерживают поэлементное присваивание массивов внутри структур, но не в целом?

87

Я понимаю, что поэлементное присваивание массивов не поддерживается, поэтому следующее не будет работать:

int num1[3] = {1,2,3};
int num2[3];
num2 = num1; // "error: invalid array assignment"

Я просто принял это как факт, полагая, что цель языка - предоставить открытый фреймворк и позволить пользователю решать, как реализовать что-то вроде копирования массива.

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

struct myStruct { int num[3]; };
struct myStruct struct1 = {{1,2,3}};
struct myStruct struct2;
struct2 = struct1;

Массив num[3]присваивается по элементам из экземпляра в struct1экземпляре в struct2.

Почему для структур поддерживается поэлементное присвоение массивов, но не в целом?

edit : Комментарий Роджера Пейта в потоке std :: string в структуре - Проблемы с копированием / назначением? кажется, указывает на общее направление ответа, но я не знаю достаточно, чтобы подтвердить это сам.

редактировать 2 : много отличных ответов. Я выбрал Лютера Блиссетта , потому что меня больше всего интересовало философское или историческое обоснование такого поведения, но ссылка Джеймса Макнеллиса на соответствующую документацию по спецификациям также была полезной.

ozmo
источник
6
Я делаю это с тегами C и C ++, потому что это происходит из C. Кроме того, хороший вопрос.
GManNickG 09
4
Возможно, стоит отметить, что давным-давно в C назначение структуры было вообще невозможно, и вам приходилось использовать memcpy()или подобное.
ggg
Немного к сведению ... boost::array( boost.org/doc/libs/release/doc/html/array.html ) и теперь std::array( en.cppreference.com/w/cpp/container/array ) - это STL-совместимые альтернативы грязные старые массивы C. Они поддерживают копирование-назначение.
Эмиль Кормье
@EmileCormier И они - тада! - структуры вокруг массивов.
Peter - Reinstate Monica

Ответы:

46

Вот мой взгляд на это:

Развитие языка C дает некоторое представление об эволюции типа массива в C:

Попробую обрисовать массив:

Предшественники C B и BCPL не имели отдельного типа массива, такого объявления как:

auto V[10] (B)
or 
let V = vec 10 (BCPL)

объявит V как (нетипизированный) указатель, который инициализируется, чтобы указывать на неиспользуемую область из 10 «слов» памяти. B уже использовался *для разыменования указателей и имел [] сокращенную нотацию, что *(V+i)означает V[i], как в C / C ++ сегодня. Однако Vэто не массив, это все же указатель, который должен указывать на некоторую память. Это вызвало проблемы, когда Деннис Ричи попытался расширить B с помощью типов структур. Он хотел, чтобы массивы были частью структур, как в C сегодня:

struct {
    int inumber;
    char name[14];
};

Но с концепцией B, BCPL массивов как указателей, это потребовало бы, чтобы nameполе содержало указатель, который должен был быть инициализирован во время выполнения в область памяти размером 14 байтов внутри структуры. Проблема инициализации / компоновки была в конечном итоге решена путем особого обращения с массивами: компилятор отслеживал расположение массивов в структурах, в стеке и т. Д., Фактически не требуя материализации указателя на данные, за исключением выражений, которые включают массивы. Эта обработка позволила почти всему B-коду продолжать работать и является источником правила «массивы преобразовываются в указатель, если вы посмотрите на них» . Это средство обеспечения совместимости, которое оказалось очень удобным, поскольку позволяло использовать массивы открытого размера и т. Д.

И вот моя догадка, почему нельзя присвоить массив: поскольку массивы были указателями в B, вы могли просто написать:

auto V[10];
V=V+5;

перебазировать "массив". Теперь это было бессмысленно, потому что основание переменной массива больше не было lvalue. Таким образом, это задание было запрещено, что помогло отловить несколько программ, которые выполняли этот перебазинг на объявленных массивах.. А потом застряло это понятие: поскольку массивы никогда не создавались для первоклассного использования системы типов C, они в основном рассматривались как особые звери, которые становятся указателями, если вы их используете. И с определенной точки зрения (которая игнорирует тот факт, что C-массивы - это неудачная попытка взлома), запрет присваивания массивов все еще имеет смысл: открытый массив или параметр функции массива обрабатываются как указатель без информации о размере. У компилятора нет информации для генерации им присвоения массива, а присвоение указателя требовалось по соображениям совместимости.

/* Example how array assignment void make things even weirder in C/C++, 
   if we don't want to break existing code.
   It's actually better to leave things as they are...
*/
typedef int vec[3];

void f(vec a, vec b) 
{
    vec x,y; 
    a=b; // pointer assignment
    x=y; // NEW! element-wise assignment
    a=x; // pointer assignment
    x=a; // NEW! element-wise assignment
}

Это не изменилось, когда версия C в 1978 году добавила назначение структур ( http://cm.bell-labs.com/cm/cs/who/dmr/cchanges.pdf ). Несмотря на то, что записи были разными типами в C, было невозможно назначить их в ранних версиях K&R C. Вам приходилось копировать их поэлементно с помощью memcpy, и вы могли передавать на них только указатели в качестве параметров функции. Присвоение (и передача параметров) теперь просто определялось как memcpy необработанной памяти структуры, и, поскольку это не могло сломать существующий код, оно было легко адаптировано. В качестве непреднамеренного побочного эффекта это неявно вводило какое-то присваивание массива, но это происходило где-то внутри структуры, поэтому это не могло действительно создать проблемы с тем, как массивы использовались.

Скандинавский мэйнфрейм
источник
Жаль, что C не определил синтаксис, например, int[10] c;чтобы lvalue cвел себя как массив из десяти элементов, а не как указатель на первый элемент массива из десяти элементов. Есть несколько ситуаций, когда полезно иметь возможность создать typedef, который выделяет пространство при использовании для переменной, но передает указатель при использовании в качестве аргумента функции, но невозможность иметь значение типа массива является значительной семантической слабостью на языке.
supercat
Вместо того чтобы говорить «указатель, который должен указывать на некоторую память», важно то, что сам указатель должен храниться в памяти как обычный указатель. Это действительно встречается в вашем последующем объяснении, но я думаю, что это лучше подчеркивает ключевое различие. (В современном C имя переменной массива действительно относится к блоку памяти, так что разница не в этом. Дело в том, что сам указатель логически не хранится нигде в абстрактной машине.)
Питер Кордес,
См . Отвращение Си к массивам, чтобы получить хорошее резюме истории.
Питер Кордес
31

Что касается операторов присваивания, стандарт C ++ говорит следующее (C ++ 03 §5.17 / 1):

Есть несколько операторов присваивания ... все требуют изменяемого lvalue в качестве левого операнда

Массив не является изменяемым lvalue.

Однако присвоение объекту типа класса определяется специально (§5.17 / 4):

Присвоение объектам класса определяется оператором присваивания копии.

Итак, мы посмотрим, что делает неявно объявленный оператор присваивания копии для класса (§12.8 / 13):

Неявно определенный оператор присваивания копии для класса X выполняет поэлементное присваивание своих подобъектов. ... Каждому подобъекту присваивается способ, соответствующий его типу:
...
- если подобъект является массивом, каждый элемент присваивается способом, соответствующим типу элемента
...

Итак, для объекта типа класса массивы копируются правильно. Обратите внимание: если вы предоставите объявленный пользователем оператор присваивания копии, вы не сможете воспользоваться этим, и вам придется копировать массив поэлементно.


Рассуждения аналогичны в C (C99 §6.5.16 / 2):

Оператор присваивания должен иметь модифицируемое значение l в качестве левого операнда.

И §6.3.2.1 / 1:

Модифицируемое lvalue - это lvalue, не имеющий типа массива ... [далее следуют другие ограничения]

В C назначение намного проще, чем в C ++ (§6.5.16.1 / 2):

При простом присваивании (=) значение правого операнда преобразуется в тип выражения присваивания и заменяет значение, хранящееся в объекте, обозначенном левым операндом.

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

Джеймс МакНеллис
источник
1
Почему массивы неизменяемы? Или, скорее, почему присвоение не определено специально для массивов, как если бы оно было в типе класса?
GManNickG 09
1
@GMan: Это более интересный вопрос, не правда ли? Для C ++ ответ, вероятно, «потому что это так в C», а для C, я полагаю, это просто из-за того, как развивался язык (т.е. причина историческая, а не техническая), но я не был жив когда большая часть этого имела место, я оставлю это кому-то более осведомленному, чтобы ответить на эту часть :-P (FWIW, я не могу найти ничего в документах с обоснованием C90 или C99).
Джеймс Макнеллис,
2
Кто-нибудь знает, где в стандарте C ++ 03 находится определение «изменяемое lvalue»? Это должно быть в §3.10. Индекс говорит, что он определен на этой странице, но это не так. В (ненормативном) примечании к §8.3.4 / 5 говорится: «Объекты типов массивов не могут быть изменены, см. 3.10», но в §3.10 ни разу не используется слово «массив».
Джеймс МакНеллис,
@ Джеймс: Я делал то же самое. Похоже, это относится к удаленному определению. И да, я всегда хотел знать настоящую причину всего этого, но это кажется загадкой. Я слышал такие вещи, как «предотвращение неэффективности людей из-за случайного присвоения массивов», но это смешно.
GManNickG 09
1
@GMan, Джеймс: Недавно было обсуждение comp.lang.c ++ groups.google.com/group/comp.lang.c++/browse_frm/thread/…, если вы пропустили это и все еще заинтересованы. По-видимому, это не потому, что массив не является изменяемым lvalue (массив, безусловно, является lvalue, и все неконстантные lvalue могут быть изменены), а потому, что =требует rvalue на RHS, а массив не может быть rvalue ! Преобразование lvalue-to-rvalue запрещено для массивов, заменено lvalue-to-pointer. static_castне лучше в создании rvalue, потому что он определяется в тех же терминах.
Potatoswatter
2

По этой ссылке: http://www2.research.att.com/~bs/bs_faq2.html есть раздел о назначении массивов:

Две основные проблемы с массивами заключаются в том, что

  • массив не знает своего размера
  • имя массива преобразуется в указатель на его первый элемент при малейшей провокации

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

Итак, компилятор не может отличить int a [10] от int b [20].

Однако структуры не обладают такой двусмысленностью.

Скотт Терли
источник
3
На этой странице говорится о передаче массивов функциям (что невозможно сделать, поэтому это просто указатель, что он имеет в виду, когда говорит, что он теряет свой размер). Это не имеет ничего общего с присвоением массивов массивам. И нет, переменная массива - это не просто «на самом деле» указатель на первый элемент, это массив. Массивы - это не указатели.
GManNickG 09
Спасибо за комментарий, но когда я прочитал этот раздел статьи, он сразу сказал, что массивы не знают своего размера, а затем использует пример, в котором массивы передаются в качестве аргументов, чтобы проиллюстрировать этот факт. Итак, когда массивы передаются в качестве аргументов, они потеряли информацию о своем размере или у них никогда не было информации для начала. Я предположил последнее.
Скотт Терли
3
Компилятор может определить разницу между двумя массивами разного размера - попробуйте напечатать sizeof(a)vs. sizeof(b)или передать aв void f(int (&)[20]);.
Георг Фриче
Важно понимать, что каждый размер массива представляет собой отдельный тип. Правила передачи параметров гарантируют, что вы можете писать «общие» функции для бедняков, которые принимают аргументы массива любого размера, за счет необходимости передавать размер отдельно. Если бы это было не так (а в C ++ вы можете - и должны! - определять ссылочные параметры для массивов определенного размера), вам потребовалась бы конкретная функция для каждого разного размера, что явно бессмысленно. Я писал об этом в другом посте .
Peter - Reinstate Monica
0

Знаю, все ответившие - знатоки C / C ++. Но я подумал, что это основная причина.

число2 = число1;

Здесь вы пытаетесь изменить базовый адрес массива, что недопустимо.

и, конечно же, struct2 = struct1;

Здесь объект struct1 назначается другому объекту.

нсивакр
источник
И назначение структур в конечном итоге назначит член массива, что вызывает тот же вопрос. Почему один разрешен, а другой нет, если в обеих ситуациях это массив?
GManNickG
1
Согласовано. Но первому препятствует компилятор (num2 = num1). Второму не препятствует компилятор. Это огромная разница.
nsivakr 09
Если бы массивы были назначаемыми, все num2 = num1было бы отлично. Элементы num2будут иметь то же значение, что и соответствующий элемент num1.
juanchopanza
0

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

Большинство функций с параметрами массива на языках, где массивы являются первоклассными типами, написаны для массивов произвольного размера. Затем функция обычно выполняет итерацию по заданному количеству элементов - информации, которую предоставляет массив. (В C идиома, конечно же, заключается в передаче указателя и отдельного количества элементов.) Функция, которая принимает массив только одного определенного размера, нужна не так часто, поэтому многое упускается. (Это меняется, когда вы можете предоставить компилятору возможность генерировать отдельную функцию для любого размера массива, как в случае с шаблонами C ++; вот почему std::arrayэто полезно.)

Питер - Восстановить Монику
источник