Чому 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.

Чому присвоєння членам масивів масивів підтримується для структур, але не в цілому?

редагувати : коментар Роджера Пате в потоці std :: string у struct - Проблеми з копіюванням / призначенням?здається, вказує на загальний напрямок відповіді, але я не знаю достатньо, щоб підтвердити це сам.

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


6
Я роблю для цього як C, так і C ++ як теги, оскільки це походить від C. Крім того, гарне запитання.
GManNickG

4
Можливо, варто зауважити, що давним-давно в С, призначення структури було загалом неможливим, і вам доводилося використовувати memcpy()або подібне.
ggg

Тільки трохи FYI ... boost::array( boost.org/doc/libs/release/doc/html/array.html ) і зараз std::array( en.cppreference.com/w/cpp/container/array ) - це сумісні зі STL альтернативи брудні старі масиви С. Вони підтримують призначення копій.
Еміль Корм'є

@EmileCormier А вони - тада! - структури навколо масивів.
Пітер - Відновити Моніку

Відповіді:


46

Ось мій погляд на це:

Розвиток мови C пропонує деяке розуміння еволюції типу масиву на мові C:

Я спробую окреслити масив:

Попередники C та BCPL не мали чіткого типу масиву, декларація типу:

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

оголосить V як (нетипізований) покажчик, який ініціалізується, щоб вказати на невикористану область з 10 "слів" пам'яті. B вже використовувався *для розмежування покажчиків і мав [] коротке позначення рук, *(V+i)мається на увазі V[i], як і в C / C ++ сьогодні. Однак Vце не масив, це все одно покажчик, який повинен вказувати на деяку пам’ять. Це спричинило проблеми, коли Денніс Річі спробував розширити B за допомогою типів struct. Він хотів, щоб масиви були частиною конструкцій, як у C сьогодні:

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

Але з концепцією масивів B, BCPL як покажчиків, це зажадало б, щоб nameполе містило покажчик, який повинен був ініціалізуватися під час виконання до області пам'яті 14 байтів у структурі. Проблема ініціалізації / компонування врешті-решт була вирішена шляхом надання масивам спеціальної обробки: компілятор відстежував би розташування масивів у структурах, у стеку тощо, фактично не вимагаючи вказівника на дані для матеріалізації, за винятком виразів, що включають масиви. Ця обробка дозволила продовжувати працювати майже усьому коду B і є джерелом правила "перетворення масивів у покажчик, якщо ви дивитесь на них" . Це хакер сумісності, який виявився дуже зручним, оскільки дозволив масиви відкритого розміру тощо.

І ось моє здогадування, чому масив неможливо призначити: Оскільки масиви були вказівниками на B, ви можете просто написати:

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

перебазувати "масив". Це тепер було безглуздо, оскільки основа змінної масиву вже не була значенням. Тож це призначення було заборонено, що допомогло вловити кілька програм, які здійснили це перебазування на оголошені масиви. І тоді це поняття застрягло: Оскільки масиви ніколи не були розроблені для того, щоб вказувати їх на першокласні системи типу С, їх переважно розглядали як особливих звірів, які стають покажчиком, якщо ви їх використовуєте. І з певної точки зору (яка ігнорує, що 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 необробленої пам'яті структури, і оскільки це не могло зламати існуючий код, воно було легко призначене. Як непередбачуваний побічний ефект, це неявно вводило якесь призначення масиву, але це траплялося десь усередині структури, тому це насправді не могло спричинити проблем із способом використання масивів.


int[10] c;Шкода, що C не визначив синтаксис, наприклад, щоб lvalue cповодився як масив із десяти елементів, а не як вказівник на перший елемент масиву з десяти елементів. Є кілька ситуацій, коли корисно мати можливість створити typedef, який виділяє простір при використанні для змінної, але передає покажчик, коли використовується як аргумент функції, але неможливість мати значення типу масиву є значною семантичною слабкістю мовою.
supercat

Замість того, щоб говорити "вказівник, який повинен вказувати на деяку пам'ять", важливим моментом є те, що сам вказівник повинен зберігатися в пам'яті як звичайний вказівник. Це справді трапляється у вашому подальшому поясненні, але я думаю, що це краще підкреслює ключову різницю. (У сучасному C назва змінної масиву справді відноситься до блоку пам'яті, тому це не різниця. Справа в тому, що сам покажчик логічно не зберігається ніде в абстрактній машині.)
Пітер Кордес,

Дивіться відразу С до масивів, щоб отримати гарний підсумок історії.
Пітер Кордес,

31

Щодо операторів присвоєння, стандарт С ++ говорить наступне (C ++ 03 §5.17 / 1):

Є декілька операторів присвоєння ... всім потрібен модифікуваний lvalue як їх лівий операнд

Масив не є змінним значенням l.

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

Присвоєння об'єктам класу визначається оператором присвоєння копії.

Отже, ми подивимось, що робить неявно оголошений оператор присвоєння копії для класу (§12.8 / 13):

Явно визначений оператор присвоєння копії для класу X виконує присвоєння членів своїх підпроектів. ... Кожному суб'єкту присвоюється спосіб, відповідний його типу:
...
- якщо субект є масивом, кожен елемент присвоюється способом, відповідним типу елемента
...

Отже, для об’єкта типу класу масиви копіюються правильно. Зверніть увагу, що якщо ви надаєте заявлений користувачем оператор присвоєння копії, ви не можете скористатися цим, і вам доведеться копіювати масив поштучно.


Міркування подібні в C (C99 §6.5.16 / 2):

Оператор присвоєння повинен мати змінне значення як лівий операнд.

І пункт 6.3.3.1 / 1:

Модифікується lvalue - це значення lvalue, яке не має типу масиву ... [інші обмеження слідують]

У C присвоєння набагато простіше, ніж у C ++ (§6.5.16.1 / 2):

У простому присвоєнні (=) значення правого операнда перетворюється на тип виразу присвоєння і замінює значення, що зберігається в об'єкті, позначеному лівим операндом.

Для призначення об’єктів структурного типу лівий та правий операнди повинні мати однаковий тип, тому значення правого операнда просто копіюється у лівий операнд.


1
Чому масиви незмінні? Вірніше, чому призначення не визначене спеціально для масивів, як це відбувається, коли воно знаходиться у класі?
GManNickG

1
@GMan: Це цікавіше питання, чи не так. Для С ++ відповідь, мабуть, "тому, що це так в С", а для С, я думаю, це пов’язано лише з тим, як еволюціонувала мова (тобто причина історична, а не технічна), але я не був живим коли більша частина цього відбулася, то я залишу дозвіл комусь більш обізнаному відповісти на цю частину :-P (FWIW, я не можу знайти нічого в обґрунтованих документах C90 або C99).
James McNellis,

2
Хто-небудь знає, де визначення "модифікується lvalue" є в стандарті C ++ 03? Це має бути в §3.10. Індекс говорить, що це визначено на цій сторінці, але це не так. У (ненормативній) примітці до §8.3.4 / 5 сказано: "Об'єкти типів масивів не можуть бути змінені, див. 3.10", але в §3.10 не один раз використовується слово "масив".
James McNellis

@James: Я просто робив те саме. Здається, це посилається на вилучене визначення. Так, я завжди хотів знати справжню причину всього цього, але це здається загадкою. Я чув про такі речі, як "заважати людям бути неефективними, випадково призначаючи масиви", але це смішно.
GManNickG

1
@GMan, Джеймс: Нещодавно відбулась дискусія на comp.lang.c ++ groups.google.com/group/comp.lang.c++/browse_frm/thread/…, якщо ви пропустили це і все ще зацікавлені. Очевидно, це не тому, що масив не є змінним значенням l (значення масиву, безумовно, є значенням, і всі неконстантні значення l є змінними), а тому, що =вимагає значення rh на RHS, а масив не може бути значенням r ! Перетворення lvalue-to-rvalue заборонено для масивів, замінених на lvalue-to-pointer. static_castне є кращим у створенні значення r, оскільки воно визначається тими самими термінами.
Potatoswatter

2

У цьому посиланні: http://www2.research.att.com/~bs/bs_faq2.html є розділ про призначення масиву:

Дві основні проблеми з масивами в тому

  • масив не знає власного розміру
  • ім'я масиву перетворюється в покажчик на його перший елемент при найменшій провокації

І я думаю, що це принципова різниця між масивами та структурами. Змінна масиву - це елемент даних низького рівня з обмеженими знаннями про себе. По суті, це шматок пам’яті та спосіб індексувати його.

Отже, компілятор не може визначити різницю між int a [10] та int b [20].

Однак структури не мають однакової двозначності.


3
Ця сторінка розповідає про передачу масивів функціям (що неможливо зробити, тому це лише вказівник, що він має на увазі, коли каже, що він втрачає свій розмір). Це не має нічого спільного з присвоєнням масивів масивам. І ні, змінна масиву - це не просто «справді» вказівник на перший елемент, це масив. Масиви не є покажчиками.
GManNickG

Дякую за коментар, але коли я прочитав цей розділ статті, він спереду каже, що масиви не знають власного розміру, а потім використовує приклад, коли масиви передаються як аргументи, щоб проілюструвати цей факт. Отже, коли масиви є передачами в якості аргументів, вони втрачали інформацію про свій розмір або у них ніколи не було інформації для початку. Я припустив останнє.
Скотт Терлі

3
Компілятор може помітити різницю між двома різними розміром масивів - спробуйте друк sizeof(a)vs. sizeof(b)або передачу aв void f(int (&)[20]);.
Georg Fritzsche

Важливо розуміти, що кожен розмір масиву є своїм типом. Правила передачі параметрів гарантують, що ви можете писати "загальні" функції бідної людини, які беруть аргументи масиву будь-якого розміру, за рахунок необхідності передавати розмір окремо. Якби це було не так (а в C ++ ви можете - і повинні! - визначати посилальні параметри для масивів певного розміру), вам потрібна буде певна функція для кожного різного розміру, що явно дурниця. Я писав про це в іншому дописі .
Пітер - Відновити Моніку

0

Я знаю, всі, хто відповів, є експертами з C / C ++. Але я думав, це головна причина.

num2 = num1;

Тут ви намагаєтеся змінити базову адресу масиву, що неприпустимо.

і звичайно, struct2 = struct1;

Тут об’єкт struct1 присвоюється іншому об’єкту.


І присвоєння структур в кінцевому підсумку призначає елемент масиву, що викликає те саме питання. Чому дозволяється одне, а не інше, коли це масив в обох ситуаціях?
GManNickG,

1
Домовились. Але першому заважає компілятор (num2 = num1). Другий компілятор не заважає. Це має величезну різницю.
nsivakr

Якби масиви можна було призначити, num2 = num1вони б чудово поводились. Елементи num2bi мали однакове значення з відповідним елементом num1.
juanchopanza

0

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.