foreach 與陣列


在處理陣列時,經常會是從頭至尾迭代陣列元素,針對這類需求,現代語言中都有 foreach 之類的語法,C++ 11 之後也有提供,然而 C 語言沒有。

透過巨集,可以來建立簡單的 foreach,以進行陣列的迭代,方式有許多種,這邊採取的出發點是以下範例:

#include <stdio.h>

int main(void) {
    int arr[] = {10, 20, 30, 40, 50}; 
    int size = *(&arr + 1) - arr;
    for(int i = 0; i < size; i++) {
        printf("%d ", *(arr + i));
    }

    return 0;
}

這邊希望的 foreach 語法,可以如下使用:

int arr[] = {10, 20, 30, 40, 50};     
foreach(int *v, arr) {
    printf("%d ", *v);
}

這個語法如何展開為方才範例的完整語法呢?因為可用的展開項目只有 int *varr,因此必須整理一下範例:

int arr[] = {10, 20, 30, 40, 50}; 
for(int size = *(&arr + 1) - arr, i = 0; i < size; i++) {
    int *v = arr + i;
    printf("%d ", *v);
}  

現在可以初步定義巨集:

#define foreach(item, arr)                                  \
    for(int size = *(&arr + 1) - arr, i = 0; i < size; i++) \
        item = (arr + i);

問題來了,你不能在巨集中直接寫死 {},因為 foreach 的使用者要能自行決定是否使用 {},因此 item = arr + i 這行必須能建立一個範疇,而 foreach 的使用者決定該範疇要不要使用 {},要能建立範疇又可以建立變數的陳述句,也只有 for 了,因此試著將範例再整理一下:

int arr[] = {10, 20, 30, 40, 50}; 
for(int size = *(&arr + 1) - arr, i = 0, j = 0; i < size; i++, j = 0) {
    for(int *v = arr + i; j < 1; j++)
        printf("%d ", *v);
}

內層的 for 作用純粹用來建立範疇,因此只執行一次,現在可以定義巨集如下:

#include <stdio.h>

#define foreach(item, arr)                                                \
    for(int size = *(&arr + 1) - arr, i = 0, j = 0; i < size; i++, j = 0) \
        for(item = arr + i; j < 1; j++)

int main(void) {
    int arr[] = {10, 20, 30, 40, 50}; 
    foreach(int *v, arr) {
        printf("%d ", *v);
    }

    return 0;
}

看來好像 OK,不過這個 foreach 在處理 break 時會有問題,底下還是會顯示全部的陣列元素:

#include <stdio.h>

#define foreach(item, arr)                                                \
    for(int size = *(&arr + 1) - arr, i = 0, j = 0; i < size; i++, j = 0) \
        for(item = arr + i; j < 1; j++)

int main(void) {
    int arr[] = {10, 20, 30, 40, 50}; 
    foreach(int *v, arr) {
        printf("%d ", *v);
        if(*v > 30) {
            break;
        }
    }

    return 0;
}

理由很簡單,展開就是 break 只中斷內部的 for 迴圈,必須有個方式知道內層 for 被中斷了,因此不採取計數 j 的方式,改用個 loop 旗標:

#include <stdio.h>

#define foreach(item, arr)                                                                  \
    for(int size = *(&arr + 1) - arr, i = 0, loop = 1; loop && i < size; i++, loop = !loop) \
        for(item = arr + i; loop; loop = !loop)

int main(void) {
    int arr[] = {10, 20, 30, 40, 50, 60}; 
    foreach(int *v, arr) {
        printf("%d ", *v);
        if(*v > 30) {
            break;
        }
    }

    return 0;
}

這個 foreach 可以處理 break,因為 loop 一開始是 1,表示預設執行外層與內層 for,內層 for 執行過後將 loop 反相,此時 loop 為 0,外層 for 又將之反相,此時 loop 又回到 1,也就可以繼續下次的迴圈。

如果內層 forbreakloop 就不會被反相,也就是 loop 維持為 1,外層 for 將之反相後 loop 成為 0,接下來外層 for 也就不會執行了。

這個 foreach 也可以用於其他型態的陣列,例如字串陣列:

#include <stdio.h>

#define foreach(item, arr)                                                                  \
    for(int size = *(&arr + 1) - arr, i = 0, loop = 1; loop && i < size; i++, loop = !loop) \
        for(item = arr + i; loop; loop = !loop)

typedef const char* String;

int main(void) {
    String names[] = {"Justin", "Monica", "Irene"};
    foreach(String *name, names) {
        printf("%s ", *name);
    }

    return 0;
}