Как работает диапазон на основе для простых массивов?

87

В C ++ 11 вы можете использовать диапазон for, который действует как foreachдругие языки. Он работает даже с простыми массивами C:

int numbers[] = { 1, 2, 3, 4, 5 };
for (int& n : numbers) {
    n *= 2;
}

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

Пол Манта
источник
10
В C или C ++ нет «динамических» массивов как таковых - есть типы массивов, а затем есть указатели, которые могут или не могут указывать на массив или динамически выделяемый блок памяти, который в основном ведет себя как массив. Для любого массива типа T [n] его размер закодирован в типе, и к нему можно получить доступ for. Но в тот момент, когда этот массив распадается на указатель, информация о размере теряется.
JohannesD
1
В вашем примере, число элементов в numbersэто sizeof(numbers)/sizeof(int), например.
JohannesD

Ответы:

57

Он работает для любого выражения, тип которого является массивом. Например:

int (*arraypointer)[4] = new int[1][4]{{1, 2, 3, 4}};
for(int &n : *arraypointer)
  n *= 2;
delete [] arraypointer;

Для более подробного объяснения, если тип выражения, переданного справа от, :является типом массива, то цикл повторяется от ptrдо ptr + size( ptrуказывая на первый элемент массива, sizeявляющийся количеством элементов массива).

Это контрастирует с пользовательскими типами, которые работают путем поиска beginи в endкачестве членов, если вы передаете объект класса или (если нет членов, вызываемых таким образом) функции, не являющиеся членами. Эти функции будут давать начальный и конечный итераторы (указывающие непосредственно после последнего элемента и начала последовательности соответственно).

Этот вопрос проясняет, почему существует такая разница.

Йоханнес Шауб - litb
источник
8
Я думаю, вопрос был в том, как это работает, а не когда это работает
см.
1
@sehe вопрос содержит несколько '?' es. Один был «Это работает с ...?». Я объяснил, как и когда это работает.
Йоханнес Шауб - лит
8
@JohannesSchaub: Я думаю, что проблема "как" здесь заключается в том, как именно вы получаете размер объекта типа массива в первую очередь (из-за путаницы с указателями и массивами, не почти все знают, что размер массива равен доступный для программиста.)
JohannesD
Я считаю, что он ищет только не-член begin`end . It just happens that std :: begin, `std::endиспользующий функции-члены, и будет использоваться, если лучшее совпадение недоступно.
Деннис Зикефуз
3
@Dennis no в Мадриде было решено изменить это и отдать предпочтение начинающим и конечным участникам. Неприятие начальных и конечных членов вызвало двусмысленность, которую трудно избежать.
Йоханнес Шауб - лит
44

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

C ++ знает размер массива, потому что это часть определения массива - это тип переменной. Компилятор должен знать тип.

Поскольку C ++ 11 std::extentможно использовать для получения размера массива:

int size1{ std::extent< char[5] >::value };
std::cout << "Array size: " << size1 << std::endl;

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

char v[] { 'A', 'B', 'C', 'D' };
int size2{ std::extent< decltype(v) >::value };
std::cout << "Array size: " << size2 << std::endl;
псур
источник
6
Это действительно то, о чем я изначально спрашивал. :)
Пол Манта
19

Согласно последнему рабочему проекту C ++ (n3376), оператор range for эквивалентен следующему:

{
    auto && __range = range-init;
    for (auto __begin = begin-expr,
              __end = end-expr;
            __begin != __end;
            ++__begin) {
        for-range-declaration = *__begin;
        statement
    }
}

Таким образом, он знает, как остановить так же, как и обычный forцикл с использованием итераторов.

Я думаю, вы можете искать что-то вроде следующего, чтобы предоставить способ использования вышеуказанного синтаксиса с массивами, которые состоят только из указателя и размера (динамические массивы):

template <typename T>
class Range
{
public:
    Range(T* collection, size_t size) :
        mCollection(collection), mSize(size)
    {
    }

    T* begin() { return &mCollection[0]; }
    T* end () { return &mCollection[mSize]; }

private:
    T* mCollection;
    size_t mSize;
};

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

for ( auto pAnimation : Range<aiAnimation*>(pScene->mAnimations, pScene->mNumAnimations) )
{
    // Do something with each pAnimation instance here
}

Этот синтаксис, на мой взгляд, намного понятнее, чем тот, который вы бы использовали std::for_eachили простой forцикл.

Грант
источник
3

Он знает, когда остановиться, потому что знает границы статических массивов.

Я не уверен, что вы имеете в виду под «динамическими массивами», в любом случае, если не выполнять итерацию по статическим массивам, неофициально, компилятор просматривает имена beginи endв области класса объекта, который вы повторяете, или просматривает до begin(range)иend(range) с помощью аргумента-зависимого поиска и используют их в качестве итераторов.

Дополнительные сведения см. В стандарте C ++ 11 (или его общедоступной версии), «6.5.4 Оператор на основе диапазона for», стр. 145

холод
источник
4
«Динамический массив» будет создан с помощью new[]. В этом случае у вас есть только указатель без указания размера, поэтому нет возможности forработать с ним на основе диапазона .
Майк Сеймур,
Мой ответ включает в себя динамический массив, размер которого (4) известен во время компиляции, но я не знаю, соответствует ли эта интерпретация «динамического массива» тому, что задал вопросник.
Йоханнес Шауб - лит
3

Как работает диапазон на основе для простых массивов?

Это должно читаться как " Скажите, что делает ранжированный (с массивами)? "

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

int ia[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};

for (auto &pl : ia)

Текстовая версия:

iaпредставляет собой массив массивов («вложенный массив»), содержащий [3]массивы, каждый из которых содержит [4]значения. В приведенном выше примере iaвыполняется цикл с помощью его основного 'range' ( [3]), и, следовательно, [3]время цикла . Каждый контур производит один из ia«S [3]первичных значений , начиная с первой и заканчивая последней - массив , содержащий [4]значения.

  • Первый цикл: plравно {1,2,3,4}- массив
  • Второй цикл: plравно {5,6,7,8}- массив
  • Третий цикл: plравно {9,10,11,12}- массив

Прежде чем мы объясним процесс, вот несколько простых напоминаний о массивах:

  • Массивы интерпретируются как указатели на их первое значение - использование массива без какой-либо итерации возвращает адрес первого значения.
  • pl должен быть ссылкой, потому что мы не можем копировать массивы
  • С массивами, когда вы добавляете число к самому объекту массива, он продвигается вперед столько раз и `` указывает '' на эквивалентную запись - если nэто число, о котором идет речь, то ia[n]это то же самое, что и *(ia+n)(Мы разыменуем адрес, который nзаписи вперед) и ia+nсовпадает с &ia[n](Мы получаем адрес этой записи в массиве).

Вот что происходит:

  • На каждом цикле, plустанавливается в качестве ссылки на ia[n], с nсравнявшись текущее значение счетчика цикла , начиная с 0. Итак, plнаходится ia[0]на первом раунде, на втором онia[1] , и так далее. Он получает значение через итерацию.
  • Цикл продолжается до тех пор, ia+nпока меньше end(ia).

... И это все.

На самом деле это просто упрощенный способ написать это :

int ia[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
for (int n = 0; n != 3; ++n)
  auto &pl = ia[n];

Если ваш массив не вложен, этот процесс становится немного проще, поскольку ссылка не требуется, потому что повторяемое значение не является массивом, а скорее `` нормальным '' значением:

 int ib[3] = {1,2,3};

 // short
 for (auto pl : ib)
   cout << pl;

 // long
 for (int n = 0; n != 3; ++n)
   cout << ib[n];

Дополнительная информация

Что, если бы мы не хотели использовать autoключевое слово при создании pl? Как бы это выглядело?

В следующем примере plотносится к array of four integers. Каждому циклу plприсваивается значение ia[n]:

int ia[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
for (int (&pl)[4] : ia)

И ... Вот как это работает, с дополнительной информацией, чтобы избавиться от путаницы. Это просто «сокращенный» forцикл, который автоматически засчитывается для вас, но не имеет возможности получить текущий цикл без выполнения этого вручную.

Супер Кот
источник
@Andy В 9 из 10 случаев заголовок совпадает с тем, что соответствует поисковому запросу в Google и других поисковых запросах. В заголовке спрашивается, как это работает? , а не когда он знает, когда остановиться? . Тем не менее , основной вопрос подразумевал будет покрыт в этом ответ на какой - то степени, и продолжает отвечать за кого - либо еще , ища другой ответ. Такие вопросы синтаксиса должны иметь заголовки, чтобы можно было написать ответ, используя только это, потому что это вся информация, необходимая искателю для поиска вопроса. Вы, конечно, не ошибаетесь - вопрос не назван так, как должно быть.
Super Cat
0

Пример кода, демонстрирующий разницу между массивами в стеке и массивами в куче


/**
 * Question: Can we use range based for built-in arrays
 * Answer: Maybe
 * 1) Yes, when array is on the Stack
 * 2) No, when array is the Heap
 * 3) Yes, When the array is on the Stack,
 *    but the array elements are on the HEAP
 */
void testStackHeapArrays() {
  int Size = 5;
  Square StackSquares[Size];  // 5 Square's on Stack
  int StackInts[Size];        // 5 int's on Stack
  // auto is Square, passed as constant reference
  for (const auto &Sq : StackSquares)
    cout << "StackSquare has length " << Sq.getLength() << endl;
  // auto is int, passed as constant reference
  // the int values are whatever is in memory!!!
  for (const auto &I : StackInts)
    cout << "StackInts value is " << I << endl;

  // Better version would be: auto HeapSquares = new Square[Size];
  Square *HeapSquares = new Square[Size];   // 5 Square's on Heap
  int *HeapInts = new int[Size];            // 5 int's on Heap

  // does not compile,
  // *HeapSquares is a pointer to the start of a memory location,
  // compiler cannot know how many Square's it has
  // for (auto &Sq : HeapSquares)
  //    cout << "HeapSquare has length " << Sq.getLength() << endl;

  // does not compile, same reason as above
  // for (const auto &I : HeapInts)
  //  cout << "HeapInts value is " << I << endl;

  // Create 3 Square objects on the Heap
  // Create an array of size-3 on the Stack with Square pointers
  // size of array is known to compiler
  Square *HeapSquares2[]{new Square(23), new Square(57), new Square(99)};
  // auto is Square*, passed as constant reference
  for (const auto &Sq : HeapSquares2)
    cout << "HeapSquare2 has length " << Sq->getLength() << endl;

  // Create 3 int objects on the Heap
  // Create an array of size-3 on the Stack with int pointers
  // size of array is known to compiler
  int *HeapInts2[]{new int(23), new int(57), new int(99)};
  // auto is int*, passed as constant reference
  for (const auto &I : HeapInts2)
    cout << "HeapInts2 has value " << *I << endl;

  delete[] HeapSquares;
  delete[] HeapInts;
  for (const auto &Sq : HeapSquares2) delete Sq;
  for (const auto &I : HeapInts2) delete I;
  // cannot delete HeapSquares2 or HeapInts2 since those arrays are on Stack
}
Yip Cubed
источник